neko/src/component/internal/websocket.ts

170 lines
4.1 KiB
TypeScript
Raw Normal View History

2020-11-06 05:15:21 +13:00
import EventEmitter from 'eventemitter3'
import { Logger } from '../utils/logger'
2021-02-09 07:14:39 +13:00
export const connTimeout = 15000
export const reconnTimeout = 1000
export const reconnMultiplier = 1.2
2020-11-06 05:15:21 +13:00
export interface NekoWebSocketEvents {
connecting: () => void
connected: () => void
disconnected: (error?: Error) => void
message: (event: string, payload: any) => void
2020-11-06 05:15:21 +13:00
}
export class NekoWebSocket extends EventEmitter<NekoWebSocketEvents> {
private _ws?: WebSocket
2021-02-09 07:14:39 +13:00
private _connTimer?: NodeJS.Timeout
2020-11-06 05:15:21 +13:00
private _log: Logger
2021-02-09 05:50:12 +13:00
private _url: string
private _token: string
2020-11-06 05:15:21 +13:00
2021-02-09 07:14:39 +13:00
// reconnection
private _reconTimer?: NodeJS.Timeout
private _reconnTimeout: number = reconnTimeout
2020-11-06 05:15:21 +13:00
constructor() {
super()
2020-11-06 05:20:37 +13:00
2020-11-06 05:15:21 +13:00
this._log = new Logger('websocket')
2021-02-09 05:50:12 +13:00
this._url = ''
this._token = ''
2021-02-09 05:50:12 +13:00
this.setUrl(location.href)
}
public setUrl(url: string) {
this._url = url.replace(/^http/, 'ws').replace(/\/+$/, '') + '/api/ws'
2020-11-06 05:15:21 +13:00
}
public setToken(token: string) {
this._token = token
}
2021-01-30 08:13:37 +13:00
get supported() {
return typeof WebSocket !== 'undefined' && WebSocket.OPEN === 1
}
2020-11-06 05:15:21 +13:00
get connected() {
return typeof this._ws !== 'undefined' && this._ws.readyState === WebSocket.OPEN
}
2021-05-10 07:06:09 +12:00
public async connect() {
2020-11-06 05:15:21 +13:00
if (this.connected) {
throw new Error('attempting to create websocket while connection open')
}
if (typeof this._ws !== 'undefined') {
this._log.debug(`previous websocket connection needs to be closed`)
this.disconnect(new Error('connection replaced'))
}
2020-11-06 05:15:21 +13:00
this.emit('connecting')
let url = this._url
if (this._token) {
url += '?token=' + encodeURIComponent(this._token)
}
2021-05-10 07:06:09 +12:00
await new Promise<void>((res, rej) => {
this._ws = new WebSocket(url)
this._log.info(`connecting`)
2020-11-06 05:15:21 +13:00
2021-05-10 07:06:09 +12:00
this._ws.onclose = rej.bind(this, new Error('connection close'))
this._ws.onerror = rej.bind(this, new Error('connection error'))
this._ws.onmessage = this.onMessage.bind(this)
2020-11-06 05:15:21 +13:00
2021-05-10 07:06:09 +12:00
this._ws.onopen = () => {
this._ws!.onclose = this.onClose.bind(this, 'close')
this._ws!.onerror = this.onClose.bind(this, 'error')
this.onConnected()
res()
}
this._connTimer = setTimeout(rej.bind(this, new Error('connection timeout')), connTimeout)
})
2020-11-06 05:15:21 +13:00
}
public disconnect(reason?: Error) {
this.emit('disconnected', reason)
2021-02-09 07:14:39 +13:00
if (this._connTimer) {
clearTimeout(this._connTimer)
2020-11-06 05:15:21 +13:00
}
if (typeof this._ws !== 'undefined') {
// unmount all events
this._ws.onopen = () => {}
this._ws.onclose = () => {}
this._ws.onerror = () => {}
this._ws.onmessage = () => {}
2020-11-06 05:15:21 +13:00
try {
this._ws.close()
2020-11-06 05:15:21 +13:00
} catch (err) {}
this._ws = undefined
}
}
public send(event: string, payload?: any) {
if (!this.connected) {
this._log.warn(`attempting to send message while disconnected`)
return
}
2020-11-06 05:20:37 +13:00
2020-11-06 05:15:21 +13:00
this._log.debug(`sending event '${event}' ${payload ? `with payload: ` : ''}`, payload)
this._ws!.send(JSON.stringify({ event, ...payload }))
}
2021-02-09 07:14:39 +13:00
private tryReconnect() {
if (this._reconTimer) {
clearTimeout(this._reconTimer)
}
this._reconTimer = setTimeout(this.onReconnect.bind(this), this._reconnTimeout)
}
2020-11-06 05:15:21 +13:00
private onMessage(e: MessageEvent) {
const { event, ...payload } = JSON.parse(e.data)
this._log.debug(`received websocket event ${event} ${payload ? `with payload: ` : ''}`, payload)
this.emit('message', event, payload)
2020-11-06 05:15:21 +13:00
}
private onConnected() {
2021-02-09 07:14:39 +13:00
if (this._connTimer) {
clearTimeout(this._connTimer)
}
if (this._reconTimer) {
clearTimeout(this._reconTimer)
2020-11-06 05:15:21 +13:00
}
if (!this.connected) {
this._log.warn(`onConnected called while being disconnected`)
return
}
2021-01-16 05:17:49 +13:00
this._log.info(`connected`)
2020-11-06 05:15:21 +13:00
this.emit('connected')
2021-02-09 07:14:39 +13:00
// reset reconnect timeout
this._reconnTimeout = reconnTimeout
2020-11-06 05:15:21 +13:00
}
2021-05-10 07:06:09 +12:00
private onClose(reason: string) {
this._log.info(`connection ${reason}`)
this.disconnect(new Error(`connection ${reason}`))
2021-02-09 07:14:39 +13:00
this.tryReconnect()
}
private onReconnect() {
this._log.info(`reconnecting after ${this._reconnTimeout}ms`)
this._reconnTimeout *= reconnMultiplier
this.connect()
2020-11-06 05:15:21 +13:00
}
}