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",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"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",
"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/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": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz",
@ -2232,6 +2250,15 @@
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"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": {
@ -2272,6 +2299,15 @@
"bin-version": "^3.0.0",
"semver": "^5.6.0",
"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": {
@ -3927,6 +3963,14 @@
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"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": {
@ -5851,6 +5895,15 @@
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"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": {
@ -6627,6 +6680,15 @@
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"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": {
@ -6695,6 +6757,15 @@
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"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": {
@ -7804,6 +7875,14 @@
"requires": {
"pify": "^4.0.1",
"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": {
@ -8404,6 +8483,14 @@
"dev": true,
"requires": {
"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": {
@ -8416,6 +8503,14 @@
"resolve": "^1.10.0",
"semver": "2 || 3 || 4 || 5",
"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": {
@ -8773,6 +8868,14 @@
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"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": {
@ -9222,6 +9325,15 @@
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"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": {
@ -10475,10 +10587,9 @@
}
},
"semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
"dev": true
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
},
"semver-regex": {
"version": "2.0.0",
@ -10495,6 +10606,15 @@
"optional": true,
"requires": {
"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": {
@ -12343,6 +12463,14 @@
"semver": "^5.3.0",
"tslib": "^1.8.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": {
@ -12393,6 +12521,14 @@
"object-assign": "^4.1.1",
"rimraf": "^2.4.4",
"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": {
@ -13096,6 +13232,14 @@
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"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": {

View File

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

View File

@ -39,7 +39,6 @@ export default function Proxies () {
const [sort, setSort] = useState(sortType.None)
const proxies = useMemo(() => {
console.log(1)
switch (sort) {
case sortType.Desc:
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 { Partial, getLocalStorageItem } from '@lib/helper'
import axios from 'axios'
import { Partial, getLocalStorageItem, to } from '@lib/helper'
import { isClashX, jsBridge } from '@lib/jsBridge'
import { createAsyncSingleton } from '@lib/asyncSingleton'
import { Log } from '@models/Log'
import { StreamReader } from './streamer'
let instance: AxiosInstance
let logsStreamReader: StreamReader<Log> = null
export interface Config {
port: number
'socks-port': number
@ -51,6 +49,19 @@ export interface Group {
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 () {
const req = await getInstance()
return req.get<Config>('configs')
@ -81,6 +92,11 @@ export async function getProxy (name: string) {
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) {
const req = await getInstance()
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 })
}
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 () {
if (isClashX()) {
const info = await jsBridge.getAPIInfo()
@ -137,14 +134,12 @@ export async function getExternalControllerConfig () {
return { hostname, port, secret }
}
export async function getLogsStreamReader () {
if (logsStreamReader) {
return logsStreamReader
}
export const getLogsStreamReader = createAsyncSingleton(async function getLogsStreamReader () {
const externalController = await getExternalControllerConfig()
const { data: config } = await getConfig()
const logUrl = `//${externalController.hostname}:${externalController.port}/logs?level=${config['log-level']}`
const auth = externalController.secret ? { Authorization: `Bearer ${externalController.secret}` } : {}
logsStreamReader = new StreamReader({ url: logUrl, bufferLength: 200, headers: auth })
return logsStreamReader
}
const [data, err] = await to(getVersion())
const version = err ? 'unkonwn version' : data.data.version
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 semver from 'semver'
import EventEmitter from 'eventemitter3'
export interface Config {
url: string
headers?: { [key: string]: string }
version: string
token?: string
bufferLength?: number
retryInterval?: number
}
@ -24,15 +26,43 @@ export class StreamReader<T> {
config
)
if (semver.valid(config.version) && semver.gt(config.version, 'v0.15.0-52-gc384693')) {
this.websocketLoop()
return
}
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 () {
const [resp, err] = await to(fetch(
this.config.url,
{
mode: 'cors',
headers: this.config.headers
headers: this.config.token ? { Authorization: `Bearer ${this.config.token}` } : {}
}
))
if (err) {

View File

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

View File

@ -8,6 +8,7 @@ import { useI18n } from '@i18n'
function useData () {
const [data, set] = useObject<Models.Data>({
version: '',
general: {},
proxy: [],
proxyGroup: [],
@ -56,6 +57,13 @@ function useData () {
proxyGroup: general.mode === 'Global' ? [proxyList] : groups as API.Group[],
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) {