socket-signal-websocket
Version:
socket-signal through simple-websocket
129 lines (106 loc) • 3.66 kB
JavaScript
import Socket from 'simple-websocket'
import assert from 'nanocustomassert'
import ReconnectingWebSocket from 'reconnecting-websocket'
import delay from 'delay'
import { SocketSignalClient } from 'socket-signal/client'
import debug from 'debug'
const log = debug('socket-signal:websocket-client')
// round robin url provider
export const defaultUrlProvider = urls => {
let urlIndex = 0
return () => {
const url = urls[urlIndex++ % urls.length]
log('url provider', url)
return url
}
}
export class SocketSignalWebsocketClient extends SocketSignalClient {
static createSocket (urlProvider, opts = {}) {
assert(Array.isArray(urlProvider) || typeof urlProvider === 'function', 'must be an array or a function')
const { simpleWebsocket = {}, reconnectingWebsocket = {} } = opts
if (Array.isArray(urlProvider)) {
urlProvider = defaultUrlProvider(urlProvider)
}
if (!reconnectingWebsocket.WebSocket) {
reconnectingWebsocket.WebSocket = SocketSignalWebsocketClient.WebSocket
}
const ws = new ReconnectingWebSocket(urlProvider, reconnectingWebsocket.protocols, reconnectingWebsocket)
const socket = new Socket({ socket: ws, ...simpleWebsocket })
const _onopen = ws.onopen
const _onclose = ws.onclose
const _onerror = ws.onerror
ws.onopen = (ev) => {
if (socket._chunk && !socket._cb) {
socket._cb = () => {}
}
_onopen(ev)
}
ws.onclose = (ev) => {
log('socket close', { shouldReconnect: ws._shouldReconnect, ev })
socket.connected = false
socket.emit('disconnect', ws._shouldReconnect)
if (!ws._shouldReconnect) {
_onclose(ev)
}
}
ws.onerror = (ev) => {
log('socket error', { shouldReconnect: ws._shouldReconnect, ev })
socket.connected = false
socket.emit('disconnect', ws._shouldReconnect)
if (!ws._shouldReconnect) {
_onerror(ev)
}
}
return { ws, socket }
}
constructor (urlProvider, opts = {}) {
const { heartbeat = false, simpleWebsocket, reconnectingWebsocket, onSocket = socket => socket, ...socketSignalOpts } = opts
let { ws, socket } = SocketSignalWebsocketClient.createSocket(urlProvider, { simpleWebsocket, reconnectingWebsocket })
socket = onSocket(socket)
super(socket, socketSignalOpts)
this._ws = ws
this._heartbeat = heartbeat
this._lastPing = 0
let reconnected = false
socket.on('connect', () => {
this.emit('connected', reconnected).catch(err => {
console.error(err)
})
reconnected = true
})
socket.on('disconnect', reconnect => {
this.emit('disconnected', reconnect).catch(err => {
console.error(err)
})
})
}
get connected () {
return this._ws.readyState === this._ws.OPEN
}
get lastPing () {
return this._lastPing
}
async _open () {
this._heartbeat && this._startHeartbeat(this._heartbeat)
return super._open()
}
async _startHeartbeat (opts = {}) {
const { interval = 10 * 1000, timeout = 5 * 1000 } = typeof opts === 'boolean' ? {} : opts
while (!(this.closed || this.closing)) {
await delay(interval)
if (this.connected && this.opened) {
const data = await this.rpc
.call('__ping__', { time: Date.now() }, { timeout })
.catch(err => {
this.emit('ping-error', err)
if (this.closed || this.closing) return
if (err.code === 'NMSG_ERR_TIMEOUT') return this._ws.reconnect()
})
if (data) {
this.emit('ping', data.time)
this._lastPing = data.time
}
}
}
}
}