From da255fa38cb6ac88fd6bab257b63e3056f26c4db Mon Sep 17 00:00:00 2001 From: Dreamacro <305009791@qq.com> Date: Mon, 30 Sep 2019 15:11:28 +0800 Subject: [PATCH] Feature: use websocket api --- package-lock.json | 152 ++++++++++++++++++++++++++++++- package.json | 2 + src/containers/Proxies/index.tsx | 1 - src/lib/asyncSingleton.ts | 29 ++++++ src/lib/request.ts | 61 ++++++------- src/lib/streamer.ts | 34 ++++++- src/models/Config.ts | 1 + src/stores/HookStore.ts | 8 ++ 8 files changed, 248 insertions(+), 40 deletions(-) create mode 100644 src/lib/asyncSingleton.ts diff --git a/package-lock.json b/package-lock.json index 7688032..bb0411d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index e94f1df..1a853ba 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/src/containers/Proxies/index.tsx b/src/containers/Proxies/index.tsx index 64c0aa3..1072e14 100644 --- a/src/containers/Proxies/index.tsx +++ b/src/containers/Proxies/index.tsx @@ -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)) diff --git a/src/lib/asyncSingleton.ts b/src/lib/asyncSingleton.ts new file mode 100644 index 0000000..cd38dbb --- /dev/null +++ b/src/lib/asyncSingleton.ts @@ -0,0 +1,29 @@ +export function createAsyncSingleton (fn: () => Promise): () => Promise { + let promise: Promise | 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() + } +} diff --git a/src/lib/request.ts b/src/lib/request.ts index f7e8e91..b1ad1b7 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -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 = 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('configs') @@ -81,6 +92,11 @@ export async function getProxy (name: string) { return req.get(`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(`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({ url: logUrl, bufferLength: 200, token: externalController.secret, version }) +}) diff --git a/src/lib/streamer.ts b/src/lib/streamer.ts index 18d71dc..3c70639 100644 --- a/src/lib/streamer.ts +++ b/src/lib/streamer.ts @@ -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 { 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) { diff --git a/src/models/Config.ts b/src/models/Config.ts index 64b191a..a1d3630 100644 --- a/src/models/Config.ts +++ b/src/models/Config.ts @@ -76,6 +76,7 @@ export interface APIInfo { } export interface Data { + version?: string general?: { diff --git a/src/stores/HookStore.ts b/src/stores/HookStore.ts index 7c830ce..675e640 100644 --- a/src/stores/HookStore.ts +++ b/src/stores/HookStore.ts @@ -8,6 +8,7 @@ import { useI18n } from '@i18n' function useData () { const [data, set] = useObject({ + 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) {