Feature: use websocket api

This commit is contained in:
Dreamacro 2019-09-30 15:11:28 +08:00
parent d66cf8f9fa
commit da255fa38c
8 changed files with 248 additions and 40 deletions

152
package-lock.json generated
View File

@ -101,6 +101,12 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true "dev": true
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
} }
} }
}, },
@ -956,6 +962,12 @@
"lodash": "^4.17.13", "lodash": "^4.17.13",
"to-fast-properties": "^2.0.0" "to-fast-properties": "^2.0.0"
} }
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
} }
} }
}, },
@ -1250,6 +1262,12 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"@types/semver": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.0.2.tgz",
"integrity": "sha512-G1Ggy7/9Nsa1Jt2yiBR2riEuyK2DFNnqow6R7cromXPMNynackRY1vqFTLz/gwnef1LHokbXThcPhqMRjUbkpQ==",
"dev": true
},
"@types/unist": { "@types/unist": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz",
@ -2232,6 +2250,15 @@
"semver": "^5.5.0", "semver": "^5.5.0",
"shebang-command": "^1.2.0", "shebang-command": "^1.2.0",
"which": "^1.2.9" "which": "^1.2.9"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true,
"optional": true
}
} }
}, },
"execa": { "execa": {
@ -2272,6 +2299,15 @@
"bin-version": "^3.0.0", "bin-version": "^3.0.0",
"semver": "^5.6.0", "semver": "^5.6.0",
"semver-truncate": "^1.1.2" "semver-truncate": "^1.1.2"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true,
"optional": true
}
} }
}, },
"bin-wrapper": { "bin-wrapper": {
@ -3927,6 +3963,14 @@
"semver": "^5.5.0", "semver": "^5.5.0",
"shebang-command": "^1.2.0", "shebang-command": "^1.2.0",
"which": "^1.2.9" "which": "^1.2.9"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
} }
}, },
"execa": { "execa": {
@ -5851,6 +5895,15 @@
"semver": "^5.5.0", "semver": "^5.5.0",
"shebang-command": "^1.2.0", "shebang-command": "^1.2.0",
"which": "^1.2.9" "which": "^1.2.9"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true,
"optional": true
}
} }
}, },
"execa": { "execa": {
@ -6627,6 +6680,15 @@
"semver": "^5.5.0", "semver": "^5.5.0",
"shebang-command": "^1.2.0", "shebang-command": "^1.2.0",
"which": "^1.2.9" "which": "^1.2.9"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true,
"optional": true
}
} }
}, },
"execa": { "execa": {
@ -6695,6 +6757,15 @@
"semver": "^5.5.0", "semver": "^5.5.0",
"shebang-command": "^1.2.0", "shebang-command": "^1.2.0",
"which": "^1.2.9" "which": "^1.2.9"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true,
"optional": true
}
} }
}, },
"execa": { "execa": {
@ -7804,6 +7875,14 @@
"requires": { "requires": {
"pify": "^4.0.1", "pify": "^4.0.1",
"semver": "^5.6.0" "semver": "^5.6.0"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
} }
}, },
"mamacro": { "mamacro": {
@ -8404,6 +8483,14 @@
"dev": true, "dev": true,
"requires": { "requires": {
"semver": "^5.3.0" "semver": "^5.3.0"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
} }
}, },
"normalize-package-data": { "normalize-package-data": {
@ -8416,6 +8503,14 @@
"resolve": "^1.10.0", "resolve": "^1.10.0",
"semver": "2 || 3 || 4 || 5", "semver": "2 || 3 || 4 || 5",
"validate-npm-package-license": "^3.0.1" "validate-npm-package-license": "^3.0.1"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
} }
}, },
"normalize-path": { "normalize-path": {
@ -8773,6 +8868,14 @@
"semver": "^5.5.0", "semver": "^5.5.0",
"shebang-command": "^1.2.0", "shebang-command": "^1.2.0",
"which": "^1.2.9" "which": "^1.2.9"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
} }
}, },
"execa": { "execa": {
@ -9222,6 +9325,15 @@
"semver": "^5.5.0", "semver": "^5.5.0",
"shebang-command": "^1.2.0", "shebang-command": "^1.2.0",
"which": "^1.2.9" "which": "^1.2.9"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true,
"optional": true
}
} }
}, },
"execa": { "execa": {
@ -10475,10 +10587,9 @@
} }
}, },
"semver": { "semver": {
"version": "5.7.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
"dev": true
}, },
"semver-regex": { "semver-regex": {
"version": "2.0.0", "version": "2.0.0",
@ -10495,6 +10606,15 @@
"optional": true, "optional": true,
"requires": { "requires": {
"semver": "^5.3.0" "semver": "^5.3.0"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true,
"optional": true
}
} }
}, },
"send": { "send": {
@ -12343,6 +12463,14 @@
"semver": "^5.3.0", "semver": "^5.3.0",
"tslib": "^1.8.0", "tslib": "^1.8.0",
"tsutils": "^2.29.0" "tsutils": "^2.29.0"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
} }
}, },
"tslint-config-standard": { "tslint-config-standard": {
@ -12393,6 +12521,14 @@
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
"rimraf": "^2.4.4", "rimraf": "^2.4.4",
"semver": "^5.3.0" "semver": "^5.3.0"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
} }
}, },
"tsutils": { "tsutils": {
@ -13096,6 +13232,14 @@
"semver": "^5.5.0", "semver": "^5.5.0",
"shebang-command": "^1.2.0", "shebang-command": "^1.2.0",
"which": "^1.2.9" "which": "^1.2.9"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
} }
}, },
"supports-color": { "supports-color": {

View File

@ -41,6 +41,7 @@
"@types/react-router-dom": "^5.1.0", "@types/react-router-dom": "^5.1.0",
"@types/react-virtualized-auto-sizer": "^1.0.0", "@types/react-virtualized-auto-sizer": "^1.0.0",
"@types/react-window": "^1.8.1", "@types/react-window": "^1.8.1",
"@types/semver": "^6.0.2",
"autoprefixer": "^9.6.1", "autoprefixer": "^9.6.1",
"awesome-typescript-loader": "^5.2.1", "awesome-typescript-loader": "^5.2.1",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
@ -83,6 +84,7 @@
"react-router-dom": "^5.1.1", "react-router-dom": "^5.1.1",
"react-virtualized-auto-sizer": "^1.0.2", "react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.5", "react-window": "^1.8.5",
"semver": "^6.3.0",
"unstated-next": "^1.1.0", "unstated-next": "^1.1.0",
"use-immer": "^0.3.4" "use-immer": "^0.3.4"
} }

View File

@ -39,7 +39,6 @@ export default function Proxies () {
const [sort, setSort] = useState(sortType.None) const [sort, setSort] = useState(sortType.None)
const proxies = useMemo(() => { const proxies = useMemo(() => {
console.log(1)
switch (sort) { switch (sort) {
case sortType.Desc: case sortType.Desc:
return data.proxy.slice().sort((a, b) => compareDesc(a, b)) return data.proxy.slice().sort((a, b) => compareDesc(a, b))

29
src/lib/asyncSingleton.ts Normal file
View File

@ -0,0 +1,29 @@
export function createAsyncSingleton<T> (fn: () => Promise<T>): () => Promise<T> {
let promise: Promise<T> | null = null
let instance: T | null = null
async function fetch () {
if (promise) {
return promise
}
promise = fn()
return promise
.then(r => {
promise = null
return r
})
.catch(e => {
promise = null
return e
})
}
return async function () {
if (instance) {
return instance
}
return fetch()
}
}

View File

@ -1,12 +1,10 @@
import axios, { AxiosInstance } from 'axios' import axios from 'axios'
import { Partial, getLocalStorageItem } from '@lib/helper' import { Partial, getLocalStorageItem, to } from '@lib/helper'
import { isClashX, jsBridge } from '@lib/jsBridge' import { isClashX, jsBridge } from '@lib/jsBridge'
import { createAsyncSingleton } from '@lib/asyncSingleton'
import { Log } from '@models/Log' import { Log } from '@models/Log'
import { StreamReader } from './streamer' import { StreamReader } from './streamer'
let instance: AxiosInstance
let logsStreamReader: StreamReader<Log> = null
export interface Config { export interface Config {
port: number port: number
'socks-port': number 'socks-port': number
@ -51,6 +49,19 @@ export interface Group {
history: History[] history: History[]
} }
export const getInstance = createAsyncSingleton(async () => {
const {
hostname,
port,
secret
} = await getExternalControllerConfig()
return axios.create({
baseURL: `//${hostname}:${port}`,
headers: secret ? { Authorization: `Bearer ${secret}` } : {}
})
})
export async function getConfig () { export async function getConfig () {
const req = await getInstance() const req = await getInstance()
return req.get<Config>('configs') return req.get<Config>('configs')
@ -81,6 +92,11 @@ export async function getProxy (name: string) {
return req.get<Proxy>(`proxies/${name}`) return req.get<Proxy>(`proxies/${name}`)
} }
export async function getVersion () {
const req = await getInstance()
return req.get<{ version: string }>('version')
}
export async function getProxyDelay (name: string) { export async function getProxyDelay (name: string) {
const req = await getInstance() const req = await getInstance()
return req.get<{ delay: number }>(`proxies/${name}/delay`, { return req.get<{ delay: number }>(`proxies/${name}/delay`, {
@ -96,25 +112,6 @@ export async function changeProxySelected (name: string, select: string) {
return req.put<void>(`proxies/${name}`, { name: select }) return req.put<void>(`proxies/${name}`, { name: select })
} }
export async function getInstance () {
if (instance) {
return instance
}
const {
hostname,
port,
secret
} = await getExternalControllerConfig()
instance = axios.create({
baseURL: `//${hostname}:${port}`,
headers: secret ? { Authorization: `Bearer ${secret}` } : {}
})
return instance
}
export async function getExternalControllerConfig () { export async function getExternalControllerConfig () {
if (isClashX()) { if (isClashX()) {
const info = await jsBridge.getAPIInfo() const info = await jsBridge.getAPIInfo()
@ -137,14 +134,12 @@ export async function getExternalControllerConfig () {
return { hostname, port, secret } return { hostname, port, secret }
} }
export async function getLogsStreamReader () { export const getLogsStreamReader = createAsyncSingleton(async function getLogsStreamReader () {
if (logsStreamReader) {
return logsStreamReader
}
const externalController = await getExternalControllerConfig() const externalController = await getExternalControllerConfig()
const { data: config } = await getConfig() const { data: config } = await getConfig()
const logUrl = `//${externalController.hostname}:${externalController.port}/logs?level=${config['log-level']}` const [data, err] = await to(getVersion())
const auth = externalController.secret ? { Authorization: `Bearer ${externalController.secret}` } : {} const version = err ? 'unkonwn version' : data.data.version
logsStreamReader = new StreamReader({ url: logUrl, bufferLength: 200, headers: auth })
return logsStreamReader const logUrl = `${location.protocol}//${externalController.hostname}:${externalController.port}/logs?level=${config['log-level']}`
} return new StreamReader<Log>({ url: logUrl, bufferLength: 200, token: externalController.secret, version })
})

View File

@ -1,9 +1,11 @@
import { to } from '@lib/helper' import { to } from '@lib/helper'
import semver from 'semver'
import EventEmitter from 'eventemitter3' import EventEmitter from 'eventemitter3'
export interface Config { export interface Config {
url: string url: string
headers?: { [key: string]: string } version: string
token?: string
bufferLength?: number bufferLength?: number
retryInterval?: number retryInterval?: number
} }
@ -24,15 +26,43 @@ export class StreamReader<T> {
config config
) )
if (semver.valid(config.version) && semver.gt(config.version, 'v0.15.0-52-gc384693')) {
this.websocketLoop()
return
}
this.loop() this.loop()
} }
protected websocketLoop () {
const url = new URL(this.config.url)
url.protocol = location.protocol === 'http:' ? 'ws:' : 'wss:'
url.searchParams.set('token', this.config.token)
const connection = new WebSocket(url.toJSON())
connection.addEventListener('message', msg => {
const data = JSON.parse(msg.data)
this.EE.emit('data', [data])
if (this.config.bufferLength > 0) {
this.innerBuffer.push(data)
if (this.innerBuffer.length > this.config.bufferLength) {
this.innerBuffer.splice(0, this.innerBuffer.length - this.config.bufferLength)
}
}
})
connection.addEventListener('close', () => setTimeout(this.websocketLoop, this.config.retryInterval))
connection.addEventListener('error', err => {
this.EE.emit('error', err)
setTimeout(this.websocketLoop, this.config.retryInterval)
})
}
protected async loop () { protected async loop () {
const [resp, err] = await to(fetch( const [resp, err] = await to(fetch(
this.config.url, this.config.url,
{ {
mode: 'cors', mode: 'cors',
headers: this.config.headers headers: this.config.token ? { Authorization: `Bearer ${this.config.token}` } : {}
} }
)) ))
if (err) { if (err) {

View File

@ -76,6 +76,7 @@ export interface APIInfo {
} }
export interface Data { export interface Data {
version?: string
general?: { general?: {

View File

@ -8,6 +8,7 @@ import { useI18n } from '@i18n'
function useData () { function useData () {
const [data, set] = useObject<Models.Data>({ const [data, set] = useObject<Models.Data>({
version: '',
general: {}, general: {},
proxy: [], proxy: [],
proxyGroup: [], proxyGroup: [],
@ -56,6 +57,13 @@ function useData () {
proxyGroup: general.mode === 'Global' ? [proxyList] : groups as API.Group[], proxyGroup: general.mode === 'Global' ? [proxyList] : groups as API.Group[],
rules: rules.data.rules rules: rules.data.rules
}) })
const [version, vErr] = await to(API.getVersion())
if (vErr) {
return
}
set('version', version.data.version)
} }
function updateDelay (proxy: string, delay: number) { function updateDelay (proxy: string, delay: number) {