diff --git a/src/components/proxies/Proxy.tsx b/src/components/proxies/Proxy.tsx index 8cc668f..47a3d54 100644 --- a/src/components/proxies/Proxy.tsx +++ b/src/components/proxies/Proxy.tsx @@ -47,7 +47,7 @@ type ProxyProps = { name: string; now?: boolean; proxy: any; - latency: any; + latency?: { number?: number }; isSelectable?: boolean; onClick?: (proxyName: string) => unknown; }; @@ -130,10 +130,10 @@ function ProxyImpl({ now, name, proxy, latency, isSelectable, onClick }: ProxyPr const className = useMemo(() => { return cx(s0.proxy, { [s0.now]: now, - [s0.error]: latency && latency.error, + // [s0.error]: latency && latency.error, [s0.selectable]: isSelectable, }); - }, [isSelectable, now, latency]); + }, [isSelectable, now]); return (
{ const proxies = getProxies(s); const delay = getDelay(s); - return { - proxy: proxies[name], - latency: delay[name], - }; + const proxy = proxies[name] || { name, type: 'Unknown', history: [] }; + return { proxy, latency: delay[name] }; }; export const Proxy = connect(mapState)(ProxyImpl); diff --git a/src/components/proxies/ProxyGroup.tsx b/src/components/proxies/ProxyGroup.tsx index 6d6c59e..857a480 100644 --- a/src/components/proxies/ProxyGroup.tsx +++ b/src/components/proxies/ProxyGroup.tsx @@ -2,7 +2,8 @@ import Tooltip from '@reach/tooltip'; import * as React from 'react'; import { useState2 } from '$src/hooks/basic'; -import { State } from '$src/store/types'; +import { DelayMapping, DispatchFn, ProxiesMapping, State } from '$src/store/types'; +import { ClashAPIConfig } from '$src/types'; import { getCollapsibleIsOpen, getHideUnavailableProxies, getProxySortBy } from '../../store/app'; import { getProxies, switchProxy } from '../../store/proxies'; @@ -16,6 +17,20 @@ import { ProxyList, ProxyListSummaryView } from './ProxyList'; const { createElement, useCallback, useMemo } = React; +type ProxyGroupImplProps = { + name: string; + all: string[]; + delay: DelayMapping; + hideUnavailableProxies: boolean; + proxySortBy: string; + proxies: ProxiesMapping; + type: string; + now: string; + isOpen: boolean; + apiConfig: ClashAPIConfig; + dispatch: DispatchFn; +}; + function ProxyGroupImpl({ name, all: allItems, @@ -28,7 +43,7 @@ function ProxyGroupImpl({ isOpen, apiConfig, dispatch, -}) { +}: ProxyGroupImplProps) { const all = useFilteredAndSorted(allItems, delay, hideUnavailableProxies, proxySortBy, proxies); const isSelectable = useMemo(() => type === 'Selector', [type]); diff --git a/src/store/proxies.tsx b/src/store/proxies.tsx index ad0a84e..5f4c0aa 100644 --- a/src/store/proxies.tsx +++ b/src/store/proxies.tsx @@ -1,8 +1,10 @@ import { atom } from 'recoil'; import { + DelayMapping, DispatchFn, FormattedProxyProvider, GetStateFn, + LatencyHistory, ProxiesMapping, ProxyItem, ProxyProvider, @@ -50,6 +52,19 @@ export const getProxyProviders = (s: State) => s.proxies.proxyProviders || []; export const getDangleProxyNames = (s: State) => s.proxies.dangleProxyNames; export const getShowModalClosePrevConns = (s: State) => s.proxies.showModalClosePrevConns; +function mapLatency(names: string[], getProxy: (name: string) => { history: LatencyHistory }) { + const result: DelayMapping = {}; + for (const name of names) { + const p = getProxy(name) || { history: [] }; + const history = p.history; + const h = history[history.length - 1]; + if (h && typeof h.delay === 'number') { + result[name] = { number: h.delay }; + } + } + return result; +} + export function fetchProxies(apiConfig: ClashAPIConfig) { return async (dispatch: any, getState: any) => { const [proxiesData, providersData] = await Promise.all([ @@ -57,36 +72,28 @@ export function fetchProxies(apiConfig: ClashAPIConfig) { proxiesAPI.fetchProviderProxies(apiConfig), ]); - const { providers: proxyProviders, proxies: providerProxies } = formatProxyProviders( - providersData.providers - ); - const proxies = { ...proxiesData.proxies, ...providerProxies }; + const { proxyProviders, providerProxyRecord } = formatProxyProviders(providersData.providers); + + const proxies = { ...providerProxyRecord, ...proxiesData.proxies }; const [groupNames, proxyNames] = retrieveGroupNamesFrom(proxies); - const delayPrev = getDelay(getState()); - const delayNext = { ...delayPrev }; - - for (let i = 0; i < proxyNames.length; i++) { - const name = proxyNames[i]; - const { history } = proxies[name] || { history: [] }; - const h = history[history.length - 1]; - if (h && typeof h.delay === 'number') { - delayNext[name] = { number: h.delay }; - } - } + const delayNext = { + ...getDelay(getState()), + ...mapLatency(Object.keys(proxies), (name) => proxies[name]), + }; // proxies that are not from a provider const dangleProxyNames = []; for (const v of proxyNames) { - if (!providerProxies[v]) dangleProxyNames.push(v); + if (!providerProxyRecord[v]) dangleProxyNames.push(v); } dispatch('store/proxies#fetchProxies', (s: State) => { s.proxies.proxies = proxies; s.proxies.groupNames = groupNames; + s.proxies.dangleProxyNames = dangleProxyNames; s.proxies.delay = delayNext; s.proxies.proxyProviders = proxyProviders; - s.proxies.dangleProxyNames = dangleProxyNames; }); }; } @@ -201,11 +208,6 @@ async function switchProxyImpl( // no wait closePrevConns(apiConfig, proxies, { groupName, itemName }); } - - /* dispatch('showModalClosePrevConns', (s: GlobalState) => { */ - /* s.proxies.showModalClosePrevConns = true; */ - /* s.proxies.switchProxyCtx = { to: { groupName, itemName } }; */ - /* }); */ } function closeModalClosePrevConns() { @@ -273,15 +275,7 @@ function requestDelayForProxyOnce(apiConfig: ClashAPIConfig, name: string) { error = res.statusText; } const { delay } = await res.json(); - - const delayPrev = getDelay(getState()); - const delayNext = { - ...delayPrev, - [name]: { - error, - number: delay, - }, - }; + const delayNext = { ...getDelay(getState()), [name]: { error, number: delay } }; dispatch('requestDelayForProxyOnce', (s) => { s.proxies.delay = delayNext; @@ -297,12 +291,33 @@ export function requestDelayForProxy(apiConfig: ClashAPIConfig, name: string) { export function requestDelayForProxies(apiConfig: ClashAPIConfig, names: string[]) { return async (dispatch: DispatchFn, getState: GetStateFn) => { - const proxyNames = getDangleProxyNames(getState()); + const proxies = getProxies(getState()); + const latencyTestUrl = getLatencyTestUrl(getState()); - const works = names - // remove names that are provided by proxy providers - .filter((p) => proxyNames.indexOf(p) > -1) - .map((p) => dispatch(requestDelayForProxy(apiConfig, p))); + const proxyDedupMap = new Map(); + const providerDedupMap = new Map(); + + const works = names.map((name) => { + const p = proxies[name]; + if (!p.__provider) { + if (proxyDedupMap.get(name)) { + return undefined; + } else { + proxyDedupMap.set(name, true); + return proxiesAPI.requestDelayForProxy(apiConfig, name, latencyTestUrl); + } + } else if (p.__provider) { + // this one is from a proxy provider + if (providerDedupMap.get(p.__provider)) { + return undefined; + } else { + providerDedupMap.set(p.__provider, true); + return healthcheckProviderByNameInternal(apiConfig, p.__provider); + } + } else { + return undefined; + } + }); await Promise.all(works); await dispatch(fetchProxies(apiConfig)); }; @@ -311,7 +326,10 @@ export function requestDelayForProxies(apiConfig: ClashAPIConfig, names: string[ export function requestDelayAll(apiConfig: ClashAPIConfig) { return async (dispatch: DispatchFn, getState: GetStateFn) => { const proxyNames = getDangleProxyNames(getState()); - await Promise.all(proxyNames.map((p) => dispatch(requestDelayForProxy(apiConfig, p)))); + const latencyTestUrl = getLatencyTestUrl(getState()); + await Promise.all( + proxyNames.map((p) => proxiesAPI.requestDelayForProxy(apiConfig, p, latencyTestUrl)) + ); const proxyProviders = getProxyProviders(getState()); // one by one for (const p of proxyProviders) { @@ -353,12 +371,13 @@ type ProvidersRaw = { }; function formatProxyProviders(providersInput: ProvidersRaw): { - providers: Array; - proxies: { [key: string]: ProxyItem }; + proxyProviders: Array; + providerProxyRecord: ProxiesMapping; } { const keys = Object.keys(providersInput); - const providers = []; - const proxies = {}; + const proxyProviders = []; + const providerProxyRecord: ProxiesMapping = {}; + for (let i = 0; i < keys.length; i++) { const provider: ProxyProvider = providersInput[keys[i]]; if (provider.name === 'default' || provider.vehicleType === 'Compatible') { @@ -368,19 +387,16 @@ function formatProxyProviders(providersInput: ProvidersRaw): { const names = []; for (let j = 0; j < proxiesArr.length; j++) { const proxy = proxiesArr[j]; - proxies[proxy.name] = proxy; + providerProxyRecord[proxy.name] = { ...proxy, __provider: provider.name }; names.push(proxy.name); } // mutate directly provider.proxies = names; - providers.push(provider); + proxyProviders.push(provider); } - return { - providers, - proxies, - }; + return { proxyProviders, providerProxyRecord }; } export const actions = { diff --git a/src/store/types.ts b/src/store/types.ts index d12adaa..b0b457e 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -27,39 +27,45 @@ export type ClashGeneralConfig = { ///// store.proxies -type LatencyHistory = Array<{ time: string; delay: number }>; -type PrimitiveProxyType = 'Shadowsocks' | 'Snell' | 'Socks5' | 'Http' | 'Vmess'; +type LatencyHistoryItem = { time: string; delay: number }; +export type LatencyHistory = LatencyHistoryItem[]; + export type ProxyItem = { name: string; - type: PrimitiveProxyType; + type: string; history: LatencyHistory; all?: string[]; now?: string; + + __provider?: string; }; + +export type ProxyDelayItem = { + number?: number; +}; + export type ProxiesMapping = Record; -export type DelayMapping = Record; +export type DelayMapping = Record; export type ProxyProvider = { name: string; type: 'Proxy'; updatedAt: string; vehicleType: 'HTTP' | 'File' | 'Compatible'; - proxies: Array; + proxies: ProxyItem[]; }; -export type FormattedProxyProvider = Omit & { - proxies: string[]; -}; +export type FormattedProxyProvider = Omit & { proxies: string[] }; export type SwitchProxyCtxItem = { groupName: string; itemName: string }; -type SwitchProxyCtx = { - to: SwitchProxyCtxItem; -}; +type SwitchProxyCtx = { to: SwitchProxyCtxItem }; + export type StateProxies = { - proxies: ProxiesMapping; - delay: DelayMapping; groupNames: string[]; proxyProviders?: FormattedProxyProvider[]; + + proxies: ProxiesMapping; + delay: DelayMapping; dangleProxyNames?: string[]; showModalClosePrevConns: boolean;