Chore: clear request client

This commit is contained in:
Dreamacro 2021-06-27 17:13:17 +08:00
parent 53f4aa4e32
commit 7e999dfdca
17 changed files with 310 additions and 328 deletions

1
src/assets/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module '*.png'

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react' import React from 'react'
import { Route, Redirect, Switch } from 'react-router-dom' import { Route, Redirect, Switch } from 'react-router-dom'
import classnames from 'classnames' import classnames from 'classnames'
import { isClashX } from '@lib/jsBridge' import { isClashX } from '@lib/jsBridge'
@ -11,15 +11,13 @@ import Settings from '@containers/Settings'
import SlideBar from '@containers/Sidebar' import SlideBar from '@containers/Sidebar'
import Connections from '@containers/Connections' import Connections from '@containers/Connections'
import ExternalControllerModal from '@containers/ExternalControllerDrawer' import ExternalControllerModal from '@containers/ExternalControllerDrawer'
import { getLogsStreamReader } from '@lib/request' import { useLogsStreamReader } from '@stores'
import '../styles/common.scss' import '../styles/common.scss'
import '../styles/iconfont.scss' import '../styles/iconfont.scss'
export default function App () { export default function App () {
useEffect(() => { useLogsStreamReader()
getLogsStreamReader()
}, [])
const routes = [ const routes = [
// { path: '/', name: 'Overview', component: Overview, exact: true }, // { path: '/', name: 'Overview', component: Overview, exact: true },

View File

@ -4,9 +4,8 @@ import classnames from 'classnames'
import { useScroll } from 'react-use' import { useScroll } from 'react-use'
import { groupBy } from 'lodash-es' import { groupBy } from 'lodash-es'
import { Header, Card, Checkbox, Modal, Icon } from '@components' import { Header, Card, Checkbox, Modal, Icon } from '@components'
import { useI18n } from '@stores' import { useClient, useConnectionStreamReader, useI18n } from '@stores'
import * as API from '@lib/request' import * as API from '@lib/request'
import { StreamReader } from '@lib/streamer'
import { useObject, useVisible } from '@lib/hook' import { useObject, useVisible } from '@lib/hook'
import { fromNow } from '@lib/date' import { fromNow } from '@lib/date'
import { RuleType } from '@models' import { RuleType } from '@models'
@ -93,6 +92,8 @@ interface formatConnection {
export default function Connections() { export default function Connections() {
const { translation, lang } = useI18n() const { translation, lang } = useI18n()
const t = useMemo(() => translation('Connections').t, [translation]) const t = useMemo(() => translation('Connections').t, [translation])
const connStreamReader = useConnectionStreamReader()
const client = useClient()
// total // total
const [traffic, setTraffic] = useObject({ const [traffic, setTraffic] = useObject({
@ -103,7 +104,7 @@ export default function Connections() {
// close all connections // close all connections
const { visible, show, hide } = useVisible() const { visible, show, hide } = useVisible()
function handleCloseConnections() { function handleCloseConnections() {
API.closeAllConnections().finally(() => hide()) client.closeAllConnections().finally(() => hide())
} }
// connections // connections
@ -161,8 +162,6 @@ export default function Connections() {
] as TableColumnOption<formatConnection>[], [t]) ] as TableColumnOption<formatConnection>[], [t])
useLayoutEffect(() => { useLayoutEffect(() => {
let streamReader: StreamReader<API.Snapshot> | null = null
function handleConnection(snapshots: API.Snapshot[]) { function handleConnection(snapshots: API.Snapshot[]) {
for (const snapshot of snapshots) { for (const snapshot of snapshots) {
setTraffic({ setTraffic({
@ -174,18 +173,12 @@ export default function Connections() {
} }
} }
(async function () { connStreamReader?.subscribe('data', handleConnection)
streamReader = await API.getConnectionStreamReader()
streamReader.subscribe('data', handleConnection)
}())
return () => { return () => {
if (streamReader) { connStreamReader?.unsubscribe('data', handleConnection)
streamReader.unsubscribe('data', handleConnection) connStreamReader?.destory()
streamReader.destory()
} }
} }, [connStreamReader, feed, setTraffic])
}, [feed, setTraffic])
const { const {
getTableProps, getTableProps,

View File

@ -1,14 +1,17 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useAtom } from 'jotai'
import { useUpdateAtom } from 'jotai/utils'
import { useObject } from '@lib/hook' import { useObject } from '@lib/hook'
import { Modal, Input, Alert } from '@components' import { Modal, Input, Alert } from '@components'
import { useI18n, useAPIInfo, useIdentity } from '@stores' import { useI18n, useAPIInfo, identityAtom } from '@stores'
import { localStorageAtom } from '@stores/request'
import './style.scss' import './style.scss'
export default function ExternalController () { export default function ExternalController () {
const { translation } = useI18n() const { translation } = useI18n()
const { t } = translation('Settings') const { t } = translation('Settings')
const { data: info, update, fetch } = useAPIInfo() const { hostname, port, secret } = useAPIInfo()
const { identity, set: setIdentity } = useIdentity() const [identity, setIdentity] = useAtom(identityAtom)
const [value, set] = useObject({ const [value, set] = useObject({
hostname: '', hostname: '',
port: '', port: '',
@ -16,16 +19,14 @@ export default function ExternalController () {
}) })
useEffect(() => { useEffect(() => {
fetch() set({ hostname, port, secret })
}, [fetch]) }, [hostname, port, secret, set])
useEffect(() => { const setter = useUpdateAtom(localStorageAtom)
set({ hostname: info.hostname, port: info.port, secret: info.secret })
}, [info, set])
function handleOk () { function handleOk () {
const { hostname, port, secret } = value const { hostname, port, secret } = value
update({ hostname, port, secret }) setter([{ hostname, port, secret }])
} }
return ( return (

View File

@ -1,9 +1,7 @@
import React, { useLayoutEffect, useEffect, useRef, useState } from 'react' import React, { useLayoutEffect, useEffect, useRef, useState } from 'react'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useI18n } from '@stores' import { useI18n, useLogsStreamReader } from '@stores'
import { Card, Header } from '@components' import { Card, Header } from '@components'
import { getLogsStreamReader } from '@lib/request'
import { StreamReader } from '@lib/streamer'
import { Log } from '@models/Log' import { Log } from '@models/Log'
import './style.scss' import './style.scss'
@ -13,6 +11,7 @@ export default function Logs () {
const [logs, setLogs] = useState<Log[]>([]) const [logs, setLogs] = useState<Log[]>([])
const { translation } = useI18n() const { translation } = useI18n()
const { t } = translation('Logs') const { t } = translation('Logs')
const logsStreamReader = useLogsStreamReader()
useLayoutEffect(() => { useLayoutEffect(() => {
const ul = listRef.current const ul = listRef.current
@ -22,22 +21,19 @@ export default function Logs () {
}) })
useEffect(() => { useEffect(() => {
let streamReader: StreamReader<Log> | null = null
function handleLog (newLogs: Log[]) { function handleLog (newLogs: Log[]) {
logsRef.current = logsRef.current.slice().concat(newLogs.map(d => ({ ...d, time: new Date() }))) logsRef.current = logsRef.current.slice().concat(newLogs.map(d => ({ ...d, time: new Date() })))
setLogs(logsRef.current) setLogs(logsRef.current)
} }
(async function () { if (logsStreamReader) {
streamReader = await getLogsStreamReader() logsStreamReader.subscribe('data', handleLog)
logsRef.current = streamReader.buffer() logsRef.current = logsStreamReader.buffer()
setLogs(logsRef.current) setLogs(logsRef.current)
streamReader.subscribe('data', handleLog) }
}())
return () => streamReader?.unsubscribe('data', handleLog) return () => logsStreamReader?.unsubscribe('data', handleLog)
}, []) }, [logsStreamReader])
return ( return (
<div className="page"> <div className="page">

View File

@ -1,7 +1,7 @@
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { useProxy, useConfig, proxyMapping } from '@stores' import { useProxy, useConfig, proxyMapping, useClient } from '@stores'
import { changeProxySelected, Group as IGroup, getConnections, closeConnection } from '@lib/request' import { Group as IGroup } from '@lib/request'
import { Tags, Tag } from '@components' import { Tags, Tag } from '@components'
import './style.scss' import './style.scss'
@ -13,14 +13,15 @@ export function Group (props: GroupProps) {
const { markProxySelected } = useProxy() const { markProxySelected } = useProxy()
const [proxyMap] = useAtom(proxyMapping) const [proxyMap] = useAtom(proxyMapping)
const { data: Config } = useConfig() const { data: Config } = useConfig()
const client = useClient()
const { config } = props const { config } = props
async function handleChangeProxySelected (name: string) { async function handleChangeProxySelected (name: string) {
await changeProxySelected(props.config.name, name) await client.changeProxySelected(props.config.name, name)
markProxySelected(props.config.name, name) markProxySelected(props.config.name, name)
if (Config.breakConnections) { if (Config.breakConnections) {
const list: string[] = [] const list: string[] = []
const snapshot = await getConnections() const snapshot = await client.getConnections()
for (const connection of snapshot.data.connections) { for (const connection of snapshot.data.connections) {
if (connection.chains.includes(props.config.name)) { if (connection.chains.includes(props.config.name)) {
list.push(connection.id) list.push(connection.id)
@ -28,7 +29,7 @@ export function Group (props: GroupProps) {
} }
for (const id of list) { for (const id of list) {
closeConnection(id) client.closeConnection(id)
} }
} }
} }

View File

@ -1,8 +1,8 @@
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { Card, Tag, Icon, Loading } from '@components' import { Card, Tag, Icon, Loading } from '@components'
import { useI18n, useProxyProviders } from '@stores' import { useClient, useI18n, useProxyProviders } from '@stores'
import { fromNow } from '@lib/date' import { fromNow } from '@lib/date'
import { Provider as IProvider, Proxy as IProxy, updateProvider, healthCheckProvider } from '@lib/request' import { Provider as IProvider, Proxy as IProxy } from '@lib/request'
import { useVisible } from '@lib/hook' import { useVisible } from '@lib/hook'
import { compareDesc } from '@containers/Proxies' import { compareDesc } from '@containers/Proxies'
import { Proxy } from '@containers/Proxies/components/Proxy' import { Proxy } from '@containers/Proxies/components/Proxy'
@ -15,6 +15,7 @@ interface ProvidersProps {
export function Provider (props: ProvidersProps) { export function Provider (props: ProvidersProps) {
const { update } = useProxyProviders() const { update } = useProxyProviders()
const { translation, lang } = useI18n() const { translation, lang } = useI18n()
const client = useClient()
const { provider } = props const { provider } = props
const { t } = translation('Proxies') const { t } = translation('Proxies')
@ -23,12 +24,12 @@ export function Provider (props: ProvidersProps) {
function handleHealthChech () { function handleHealthChech () {
show() show()
healthCheckProvider(provider.name).then(() => update()).finally(() => hide()) client.healthCheckProvider(provider.name).then(() => update()).finally(() => hide())
} }
function handleUpdate () { function handleUpdate () {
show() show()
updateProvider(provider.name).then(() => update()).finally(() => hide()) client.updateProvider(provider.name).then(() => update()).finally(() => hide())
} }
const proxies = useMemo(() => { const proxies = useMemo(() => {

View File

@ -3,8 +3,8 @@ import { ResultAsync } from 'neverthrow'
import type{ AxiosError } from 'axios' import type{ AxiosError } from 'axios'
import classnames from 'classnames' import classnames from 'classnames'
import { BaseComponentProps } from '@models' import { BaseComponentProps } from '@models'
import { useProxy } from '@stores' import { useClient, useProxy } from '@stores'
import { getProxyDelay, Proxy as IProxy } from '@lib/request' import { Proxy as IProxy } from '@lib/request'
import EE, { Action } from '@lib/event' import EE, { Action } from '@lib/event'
import { isClashX, jsBridge } from '@lib/jsBridge' import { isClashX, jsBridge } from '@lib/jsBridge'
@ -21,19 +21,20 @@ const TagColors = {
'#ff3e5e': Infinity '#ff3e5e': Infinity
} }
async function getDelay (name: string) { export function Proxy (props: ProxyProps) {
const { config, className } = props
const { set } = useProxy()
const client = useClient()
const getDelay = useCallback(async (name: string) => {
if (isClashX()) { if (isClashX()) {
const delay = await jsBridge?.getProxyDelay(name) ?? 0 const delay = await jsBridge?.getProxyDelay(name) ?? 0
return delay return delay
} }
const { data: { delay } } = await getProxyDelay(name) const { data: { delay } } = await client.getProxyDelay(name)
return delay return delay
} }, [client])
export function Proxy (props: ProxyProps) {
const { config, className } = props
const { set } = useProxy()
const speedTest = useCallback(async function () { const speedTest = useCallback(async function () {
const result = await ResultAsync.fromPromise(getDelay(config.name), e => e as AxiosError) const result = await ResultAsync.fromPromise(getDelay(config.name), e => e as AxiosError)
@ -45,7 +46,7 @@ export function Proxy (props: ProxyProps) {
proxy.history.push({ time: Date.now().toString(), delay: validDelay }) proxy.history.push({ time: Date.now().toString(), delay: validDelay })
} }
}) })
}, [config.name, set]) }, [config.name, getDelay, set])
const delay = useMemo( const delay = useMemo(
() => config.history?.length ? config.history.slice(-1)[0].delay : 0, () => config.history?.length ? config.history.slice(-1)[0].delay : 0,

View File

@ -1,9 +1,9 @@
import * as React from 'react' import * as React from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import { Card, Tag, Icon } from '@components' import { Card, Tag, Icon } from '@components'
import { useI18n, useRuleProviders } from '@stores' import { useClient, useI18n, useRuleProviders } from '@stores'
import { fromNow } from '@lib/date' import { fromNow } from '@lib/date'
import { RuleProvider, updateRuleProvider } from '@lib/request' import { RuleProvider } from '@lib/request'
import { useVisible } from '@lib/hook' import { useVisible } from '@lib/hook'
import './style.scss' import './style.scss'
@ -14,6 +14,7 @@ interface ProvidersProps {
export function Provider (props: ProvidersProps) { export function Provider (props: ProvidersProps) {
const { update } = useRuleProviders() const { update } = useRuleProviders()
const { translation, lang } = useI18n() const { translation, lang } = useI18n()
const client = useClient()
const { provider } = props const { provider } = props
const { t } = translation('Rules') const { t } = translation('Rules')
@ -22,7 +23,7 @@ export function Provider (props: ProvidersProps) {
function handleUpdate () { function handleUpdate () {
show() show()
updateRuleProvider(provider.name).then(() => update()).finally(() => hide()) client.updateRuleProvider(provider.name).then(() => update()).finally(() => hide())
} }
const updateClassnames = classnames('rule-provider-icon', { 'rule-provider-loading': visible }) const updateClassnames = classnames('rule-provider-icon', { 'rule-provider-loading': visible })

View File

@ -1,9 +1,9 @@
import React, { useEffect, useMemo } from 'react' import React, { useEffect, useMemo } from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import { useUpdateAtom } from 'jotai/utils'
import { capitalize } from 'lodash-es' import { capitalize } from 'lodash-es'
import { Header, Card, Switch, ButtonSelect, ButtonSelectOptions, Input } from '@components' import { Header, Card, Switch, ButtonSelect, ButtonSelectOptions, Input } from '@components'
import { useI18n, useClashXData, useAPIInfo, useGeneral, useIdentity, useVersion } from '@stores' import { useI18n, useClashXData, useAPIInfo, useGeneral, useVersion, useClient, identityAtom } from '@stores'
import { updateConfig } from '@lib/request'
import { useObject } from '@lib/hook' import { useObject } from '@lib/hook'
import { jsBridge } from '@lib/jsBridge' import { jsBridge } from '@lib/jsBridge'
import { Lang } from '@i18n' import { Lang } from '@i18n'
@ -15,10 +15,11 @@ export default function Settings () {
const { premium } = useVersion() const { premium } = useVersion()
const { data: clashXData, update: fetchClashXData } = useClashXData() const { data: clashXData, update: fetchClashXData } = useClashXData()
const { general, update: fetchGeneral } = useGeneral() const { general, update: fetchGeneral } = useGeneral()
const { set: setIdentity } = useIdentity() const setIdentity = useUpdateAtom(identityAtom)
const { data: apiInfo } = useAPIInfo() const apiInfo = useAPIInfo()
const { translation, setLang, lang } = useI18n() const { translation, setLang, lang } = useI18n()
const { t } = translation('Settings') const { t } = translation('Settings')
const client = useClient()
const [info, set] = useObject({ const [info, set] = useObject({
socks5ProxyPort: 7891, socks5ProxyPort: 7891,
httpProxyPort: 7890, httpProxyPort: 7890,
@ -32,7 +33,7 @@ export default function Settings () {
}, [general, set]) }, [general, set])
async function handleProxyModeChange (mode: string) { async function handleProxyModeChange (mode: string) {
await updateConfig({ mode }) await client.updateConfig({ mode })
await fetchGeneral() await fetchGeneral()
} }
@ -51,22 +52,22 @@ export default function Settings () {
} }
async function handleHttpPortSave () { async function handleHttpPortSave () {
await updateConfig({ port: info.httpProxyPort }) await client.updateConfig({ port: info.httpProxyPort })
await fetchGeneral() await fetchGeneral()
} }
async function handleSocksPortSave () { async function handleSocksPortSave () {
await updateConfig({ 'socks-port': info.socks5ProxyPort }) await client.updateConfig({ 'socks-port': info.socks5ProxyPort })
await fetchGeneral() await fetchGeneral()
} }
async function handleMixedPortSave () { async function handleMixedPortSave () {
await updateConfig({ 'mixed-port': info.mixedProxyPort }) await client.updateConfig({ 'mixed-port': info.mixedProxyPort })
await fetchGeneral() await fetchGeneral()
} }
async function handleAllowLanChange (state: boolean) { async function handleAllowLanChange (state: boolean) {
await updateConfig({ 'allow-lan': state }) await client.updateConfig({ 'allow-lan': state })
await fetchGeneral() await fetchGeneral()
} }

View File

@ -3,9 +3,8 @@ import { NavLink } from 'react-router-dom'
import classnames from 'classnames' import classnames from 'classnames'
import { useI18n, useVersion, useClashXData } from '@stores' import { useI18n, useVersion, useClashXData } from '@stores'
import './style.scss'
import logo from '@assets/logo.png' import logo from '@assets/logo.png'
import useSWR from 'swr' import './style.scss'
interface SidebarProps { interface SidebarProps {
routes: { routes: {
@ -19,12 +18,10 @@ interface SidebarProps {
export default function Sidebar (props: SidebarProps) { export default function Sidebar (props: SidebarProps) {
const { routes } = props const { routes } = props
const { translation } = useI18n() const { translation } = useI18n()
const { version, premium, update } = useVersion() const { version, premium } = useVersion()
const { data } = useClashXData() const { data } = useClashXData()
const { t } = translation('SideBar') const { t } = translation('SideBar')
useSWR('version', update)
const navlinks = routes.map( const navlinks = routes.map(
({ path, name, exact, noMobile }) => ( ({ path, name, exact, noMobile }) => (
<li className={classnames('item', { 'no-mobile': noMobile })} key={name}> <li className={classnames('item', { 'no-mobile': noMobile })} key={name}>

View File

@ -1,17 +1,5 @@
export function getLocalStorageItem (key: string, defaultValue = '') {
return window.localStorage.getItem(key) || defaultValue
}
export function setLocalStorageItem (key: string, value: string) {
return window.localStorage.setItem(key, value)
}
export function noop () {} export function noop () {}
export function getSearchParam(key: string) {
return new URLSearchParams(window.location.search).get(key)
}
export function partition<T> (arr: T[], fn: (arg: T) => boolean): [T[], T[]] { export function partition<T> (arr: T[], fn: (arg: T) => boolean): [T[], T[]] {
const left: T[] = [] const left: T[] = []
const right: T[] = [] const right: T[] = []

View File

@ -1,10 +1,4 @@
import axios, { AxiosError } from 'axios' import axios, { AxiosInstance } from 'axios'
import { ResultAsync } from 'neverthrow'
import { getLocalStorageItem, getSearchParam } from '@lib/helper'
import { isClashX, jsBridge } from '@lib/jsBridge'
import { createAsyncSingleton } from '@lib/asyncSingleton'
import { Log } from '@models/Log'
import { StreamReader } from './streamer'
export interface Config { export interface Config {
port: number port: number
@ -99,172 +93,90 @@ export interface Connections {
rulePayload: string rulePayload: string
} }
export async function getExternalControllerConfig () { export class Client {
if (isClashX()) { private axiosClient: AxiosInstance
const info = await jsBridge!.getAPIInfo() constructor(url: string, secret?: string) {
this.axiosClient = axios.create({
return { baseURL: url,
hostname: info.host,
port: info.port,
secret: info.secret,
protocol: 'http:'
}
}
let url: URL | undefined;
{
const meta = document.querySelector<HTMLMetaElement>('meta[name="external-controller"]')
if (meta?.content?.match(/^https?:/)) {
// [protocol]://[secret]@[hostname]:[port]
url = new URL(meta.content)
}
}
const hostname = getSearchParam('host') ?? getLocalStorageItem('externalControllerAddr', url?.hostname ?? '127.0.0.1')
const port = getSearchParam('port') ?? getLocalStorageItem('externalControllerPort', url?.port ?? '9090')
const secret = getSearchParam('secret') ?? getLocalStorageItem('secret', url?.username ?? '')
const protocol = getSearchParam('protocol') ?? hostname === '127.0.0.1' ? 'http:' : (url?.protocol ?? window.location.protocol)
if (!hostname || !port) {
throw new Error('can\'t get hostname or port')
}
return { hostname, port, secret, protocol }
}
export const getInstance = createAsyncSingleton(async () => {
const {
hostname,
port,
secret,
protocol
} = await getExternalControllerConfig()
return axios.create({
baseURL: `${protocol}//${hostname}:${port}`,
headers: secret ? { Authorization: `Bearer ${secret}` } : {} headers: secret ? { Authorization: `Bearer ${secret}` } : {}
}) })
}) }
export async function getConfig () { getConfig() {
const req = await getInstance() return this.axiosClient.get<Config>('configs')
return req.get<Config>('configs') }
}
export async function updateConfig (config: Partial<Config>) { updateConfig(config: Partial<Config>) {
const req = await getInstance() return this.axiosClient.patch<void>('configs', config)
return req.patch<void>('configs', config) }
}
export async function getRules () { getRules() {
const req = await getInstance() return this.axiosClient.get<Rules>('rules')
return req.get<Rules>('rules') }
}
export async function updateRules () { async getProxyProviders () {
const req = await getInstance() const resp = await this.axiosClient.get<ProxyProviders>('providers/proxies', {
return req.put<void>('rules') validateStatus(status) {
}
export async function getProxyProviders () {
const req = await getInstance()
return req.get<ProxyProviders>('providers/proxies', {
validateStatus (status) {
// compatible old version // compatible old version
return (status >= 200 && status < 300) || status === 404 return (status >= 200 && status < 300) || status === 404
} }
}) })
// compatible old version
.then(resp => {
if (resp.status === 404) { if (resp.status === 404) {
resp.data = { providers: {} } resp.data = { providers: {} }
} }
return resp return resp
}) }
}
export async function getRuleProviders () { getRuleProviders () {
const req = await getInstance() return this.axiosClient.get<RuleProviders>('providers/rules')
return req.get<RuleProviders>('providers/rules') }
}
export async function updateProvider (name: string) { updateProvider (name: string) {
const req = await getInstance() return this.axiosClient.put<void>(`providers/proxies/${encodeURIComponent(name)}`)
return req.put<void>(`providers/proxies/${encodeURIComponent(name)}`) }
}
export async function updateRuleProvider (name: string) { updateRuleProvider (name: string) {
const req = await getInstance() return this.axiosClient.put<void>(`providers/rules/${encodeURIComponent(name)}`)
return req.put<void>(`providers/rules/${encodeURIComponent(name)}`) }
}
export async function healthCheckProvider (name: string) { healthCheckProvider (name: string) {
const req = await getInstance() return this.axiosClient.get<void>(`providers/proxies/${encodeURIComponent(name)}/healthcheck`)
return req.get<void>(`providers/proxies/${encodeURIComponent(name)}/healthcheck`) }
}
export async function getProxies () { getProxies () {
const req = await getInstance() return this.axiosClient.get<Proxies>('proxies')
return req.get<Proxies>('proxies') }
}
export async function getProxy (name: string) { getProxy (name: string) {
const req = await getInstance() return this.axiosClient.get<Proxy>(`proxies/${encodeURIComponent(name)}`)
return req.get<Proxy>(`proxies/${encodeURIComponent(name)}`) }
}
export async function getVersion () { getVersion () {
const req = await getInstance() return this.axiosClient.get<{ version: string, premium?: boolean }>('version')
return req.get<{ version: string, premium?: boolean }>('version') }
}
export async function getProxyDelay (name: string) { getProxyDelay (name: string) {
const req = await getInstance() return this.axiosClient.get<{ delay: number }>(`proxies/${encodeURIComponent(name)}/delay`, {
return req.get<{ delay: number }>(`proxies/${encodeURIComponent(name)}/delay`, {
params: { params: {
timeout: 5000, timeout: 5000,
url: 'http://www.gstatic.com/generate_204' url: 'http://www.gstatic.com/generate_204'
} }
}) })
}
closeAllConnections () {
return this.axiosClient.delete('connections')
}
closeConnection (id: string) {
return this.axiosClient.delete(`connections/${id}`)
}
getConnections () {
return this.axiosClient.get<Snapshot>('connections')
}
changeProxySelected (name: string, select: string) {
return this.axiosClient.put<void>(`proxies/${encodeURIComponent(name)}`, { name: select })
}
} }
export async function closeAllConnections () {
const req = await getInstance()
return req.delete('connections')
}
export async function closeConnection (id: string) {
const req = await getInstance()
return req.delete(`connections/${id}`)
}
export async function getConnections () {
const req = await getInstance()
return req.get<Snapshot>('connections')
}
export async function changeProxySelected (name: string, select: string) {
const req = await getInstance()
return req.put<void>(`proxies/${encodeURIComponent(name)}`, { name: select })
}
export const getLogsStreamReader = createAsyncSingleton(async function () {
const externalController = await getExternalControllerConfig()
const { data: config } = await getConfig()
const result = await ResultAsync.fromPromise(getVersion(), err => err as AxiosError)
const version = result.isErr() ? 'unkonwn version' : result.value.data.version
const useWebsocket = !!version || true
const logUrl = `${externalController.protocol}//${externalController.hostname}:${externalController.port}/logs?level=${config['log-level']}`
return new StreamReader<Log>({ url: logUrl, bufferLength: 200, token: externalController.secret, useWebsocket })
})
export const getConnectionStreamReader = createAsyncSingleton(async function () {
const externalController = await getExternalControllerConfig()
const result = await ResultAsync.fromPromise(getVersion(), err => err as AxiosError)
const version = result.isErr() ? 'unkonwn version' : result.value.data.version
const useWebsocket = !!version || true
const logUrl = `${externalController.protocol}//${externalController.hostname}:${externalController.port}/connections`
return new StreamReader<Snapshot>({ url: logUrl, bufferLength: 200, token: externalController.secret, useWebsocket })
})

View File

@ -1,14 +1,17 @@
import React from 'react' import React, { Suspense } from 'react'
import { render } from 'react-dom' import { render } from 'react-dom'
import { HashRouter } from 'react-router-dom' import { HashRouter } from 'react-router-dom'
import App from '@containers/App' import App from '@containers/App'
import { Loading } from '@components'
import 'virtual:windi.css' import 'virtual:windi.css'
export default function renderApp () { export default function renderApp () {
const rootEl = document.getElementById('root') const rootEl = document.getElementById('root')
const AppInstance = ( const AppInstance = (
<HashRouter> <HashRouter>
<Suspense fallback={<Loading visible />}>
<App /> <App />
</Suspense>
</HashRouter> </HashRouter>
) )

View File

@ -1 +1,2 @@
export * from './jotai' export * from './jotai'
export * from './request'

View File

@ -1,7 +1,7 @@
import { ResultAsync } from 'neverthrow' import { ResultAsync } from 'neverthrow'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { atom, useAtom } from 'jotai' import { atom, useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils' import { atomWithStorage, useUpdateAtom } from 'jotai/utils'
import { atomWithImmer } from 'jotai/immer' import { atomWithImmer } from 'jotai/immer'
import { useCallback, useEffect, useMemo } from 'react' import { useCallback, useEffect, useMemo } from 'react'
import { get } from 'lodash-es' import { get } from 'lodash-es'
@ -12,33 +12,14 @@ import { Language, locales, Lang, getDefaultLanguage } from '@i18n'
import { useWarpImmerSetter, WritableDraft } from '@lib/jotai' import { useWarpImmerSetter, WritableDraft } from '@lib/jotai'
import * as API from '@lib/request' import * as API from '@lib/request'
import * as Models from '@models' import * as Models from '@models'
import { partition, setLocalStorageItem } from '@lib/helper' import { partition } from '@lib/helper'
import { isClashX, jsBridge } from '@lib/jsBridge' import { isClashX, jsBridge } from '@lib/jsBridge'
import { useAPIInfo, useClient } from './request'
import { StreamReader } from '@lib/streamer'
import { Log } from '@models/Log'
import { Snapshot } from '@lib/request'
const identity = atom(true) export const identityAtom = atom(true)
type AsyncFunction<A, O> = (...args: A[]) => Promise<O>
export function useIdentity () {
const [id, set] = useAtom(identity)
function wrapFetcher<A, O> (fn: AsyncFunction<A, O>) {
return async function (...args: A[]) {
const result = await ResultAsync.fromPromise(fn(...args), e => e as AxiosError)
if (result.isErr()) {
if (result.error.response?.status === 401) {
set(false)
}
throw result.error
}
set(true)
return result.value
}
}
return { identity: id, wrapFetcher, set }
}
export const languageAtom = atomWithStorage<Lang | undefined>('language', undefined) export const languageAtom = atomWithStorage<Lang | undefined>('language', undefined)
@ -66,10 +47,11 @@ export const version = atom({
export function useVersion () { export function useVersion () {
const [data, set] = useAtom(version) const [data, set] = useAtom(version)
const { set: setIdentity } = useIdentity() const client = useClient()
const setIdentity = useUpdateAtom(identityAtom)
async function update () { useSWR([client], async function () {
const result = await ResultAsync.fromPromise(API.getVersion(), e => e as AxiosError) const result = await ResultAsync.fromPromise(client.getVersion(), e => e as AxiosError)
setIdentity(result.isOk()) setIdentity(result.isOk())
set( set(
@ -77,20 +59,21 @@ export function useVersion () {
? { version: '', premium: false } ? { version: '', premium: false }
: { version: result.value.data.version, premium: !!result.value.data.premium } : { version: result.value.data.version, premium: !!result.value.data.premium }
) )
} })
return { version: data.version, premium: data.premium, update } return { version: data.version, premium: data.premium }
} }
export function useRuleProviders () { export function useRuleProviders () {
const [{ premium }] = useAtom(version) const [{ premium }] = useAtom(version)
const client = useClient()
const { data, mutate } = useSWR('/providers/rule', async () => { const { data, mutate } = useSWR(['/providers/rule', client], async () => {
if (!premium) { if (!premium) {
return [] return []
} }
const ruleProviders = await API.getRuleProviders() const ruleProviders = await client.getRuleProviders()
return Object.keys(ruleProviders.data.providers) return Object.keys(ruleProviders.data.providers)
.map<API.RuleProvider>(name => ruleProviders.data.providers[name]) .map<API.RuleProvider>(name => ruleProviders.data.providers[name])
@ -117,9 +100,10 @@ export const proxyProvider = atom([] as API.Provider[])
export function useProxyProviders () { export function useProxyProviders () {
const [providers, set] = useAtom(proxyProvider) const [providers, set] = useAtom(proxyProvider)
const client = useClient()
const { data, mutate } = useSWR('/providers/proxy', async () => { const { data, mutate } = useSWR(['/providers/proxy', client], async () => {
const proxyProviders = await API.getProxyProviders() const proxyProviders = await client.getProxyProviders()
return Object.keys(proxyProviders.data.providers) return Object.keys(proxyProviders.data.providers)
.map<API.Provider>(name => proxyProviders.data.providers[name]) .map<API.Provider>(name => proxyProviders.data.providers[name])
@ -132,8 +116,10 @@ export function useProxyProviders () {
} }
export function useGeneral () { export function useGeneral () {
const { data, mutate } = useSWR('/config', async () => { const client = useClient()
const resp = await API.getConfig()
const { data, mutate } = useSWR(['/config', client], async () => {
const resp = await client.getConfig()
const data = resp.data const data = resp.data
return { return {
port: data.port, port: data.port,
@ -164,9 +150,10 @@ export const proxies = atomWithImmer({
export function useProxy () { export function useProxy () {
const [allProxy, rawSet] = useAtom(proxies) const [allProxy, rawSet] = useAtom(proxies)
const set = useWarpImmerSetter(rawSet) const set = useWarpImmerSetter(rawSet)
const client = useClient()
const { mutate } = useSWR('/proxies', async () => { const { mutate } = useSWR(['/proxies', client], async () => {
const allProxies = await API.getProxies() const allProxies = await client.getProxies()
const global = allProxies.data.proxies.GLOBAL as API.Group const global = allProxies.data.proxies.GLOBAL as API.Group
// fix missing name // fix missing name
@ -240,42 +227,64 @@ export function useClashXData () {
return { data, update: mutate } return { data, update: mutate }
} }
export const apiData = atom({
hostname: '127.0.0.1',
port: '9090',
secret: ''
})
export function useAPIInfo () {
const [data, set] = useAtom(apiData)
const fetch = useCallback(async function fetch () {
const info = await API.getExternalControllerConfig()
set({ ...info })
}, [set])
async function update (info: typeof data) {
const { hostname, port, secret } = info
setLocalStorageItem('externalControllerAddr', hostname)
setLocalStorageItem('externalControllerPort', port)
setLocalStorageItem('secret', secret)
window.location.reload()
}
return { data, fetch, update }
}
export const rules = atomWithImmer([] as API.Rule[]) export const rules = atomWithImmer([] as API.Rule[])
export function useRule () { export function useRule () {
const [data, rawSet] = useAtom(rules) const [data, rawSet] = useAtom(rules)
const set = useWarpImmerSetter(rawSet) const set = useWarpImmerSetter(rawSet)
const client = useClient()
async function update () { async function update () {
const resp = await API.getRules() const resp = await client.getRules()
set(resp.data.rules) set(resp.data.rules)
} }
return { rules: data, update } return { rules: data, update }
} }
const logsAtom = atom({
key: '',
instance: null as StreamReader<Log> | null
})
export function useLogsStreamReader () {
const apiInfo = useAPIInfo()
const { general } = useGeneral()
const version = useVersion()
const [item, setItem] = useAtom(logsAtom)
if (!version.version) {
return null
}
const useWebsocket = !!version.version || true
const key = `${apiInfo.protocol}//${apiInfo.hostname}:${apiInfo.port}/logs?level=${general.logLevel ?? ''}&useWebsocket=${useWebsocket}&secret=${apiInfo.secret}`
if (item.key === key) {
return item.instance!
}
const oldInstance = item.instance
const logUrl = `${apiInfo.protocol}//${apiInfo.hostname}:${apiInfo.port}/logs?level=${general.logLevel ?? ''}`
const instance = new StreamReader<Log>({ url: logUrl, bufferLength: 200, token: apiInfo.secret, useWebsocket })
setItem({ key, instance })
if (oldInstance) {
oldInstance.destory()
}
return instance
}
export function useConnectionStreamReader () {
const apiInfo = useAPIInfo()
const version = useVersion()
const useWebsocket = !!version.version || true
const url = `${apiInfo.protocol}//${apiInfo.hostname}:${apiInfo.port}/connections`
return useMemo(
() => version.version ? new StreamReader<Snapshot>({ url, bufferLength: 200, token: apiInfo.secret, useWebsocket }) : null,
[apiInfo.secret, url, useWebsocket, version.version]
)
}

78
src/stores/request.ts Normal file
View File

@ -0,0 +1,78 @@
import { atom, useAtom } from "jotai";
import { isClashX, jsBridge } from "@lib/jsBridge";
import { atomWithStorage, useAtomValue } from "jotai/utils";
import { useLocation } from "react-use";
import { Client } from "@lib/request";
const clashxConfigAtom = atom(async () => {
if (!isClashX()) {
return null
}
const info = await jsBridge!.getAPIInfo()
return {
hostname: info.host,
port: info.port,
secret: info.secret,
protocol: 'http:'
}
})
export const localStorageAtom = atomWithStorage<{
hostname: string;
port: string;
secret: string;
}[]>('externalControllers', [])
export function useAPIInfo() {
const clashx = useAtomValue(clashxConfigAtom)
const location = useLocation()
const localStorage = useAtomValue(localStorageAtom)
if (clashx) {
return clashx
}
let url: URL | undefined;
{
const meta = document.querySelector<HTMLMetaElement>('meta[name="external-controller"]')
if (meta?.content?.match(/^https?:/)) {
// [protocol]://[secret]@[hostname]:[port]
url = new URL(meta.content)
}
}
const qs = new URLSearchParams(location.search)
const hostname = qs.get('host') ?? localStorage?.[0]?.hostname ?? url?.hostname ?? '127.0.0.1'
const port = qs.get('port') ?? localStorage?.[0]?.port ?? url?.port ?? '9090'
const secret = qs.get('secret') ?? localStorage?.[0]?.secret ?? url?.username ?? ''
const protocol = qs.get('protocol') ?? hostname === '127.0.0.1' ? 'http:' : (url?.protocol ?? window.location.protocol)
return { hostname, port, secret, protocol }
}
const clientAtom = atom({
key: '',
instance: null as Client | null
})
export function useClient() {
const {
hostname,
port,
secret,
protocol
} = useAPIInfo()
const [item, setItem] = useAtom(clientAtom)
const key = `${protocol}//${hostname}:${port}?secret=${secret}`
if (item.key === key) {
return item.instance!
}
const client = new Client(`${protocol}//${hostname}:${port}`, secret)
setItem({ key, instance: client })
return client
}