/* eslint-env browser */ /** * Tiny websocket connection handler. * * Implements exponential backoff reconnects, ping/pong, and a nice event system using [lib0/observable]. * * @module websocket */ import { Observable } from './observable.js' import * as time from './time.js' import * as math from './math.js' const reconnectTimeoutBase = 1200 const maxReconnectTimeout = 2500 // @todo - this should depend on awareness.outdatedTime const messageReconnectTimeout = 30000 /** * @param {WebsocketClient} wsclient */ const setupWS = (wsclient) => { if (wsclient.shouldConnect && wsclient.ws === null) { const websocket = new WebSocket(wsclient.url) const binaryType = wsclient.binaryType /** * @type {any} */ let pingTimeout = null if (binaryType) { websocket.binaryType = binaryType } wsclient.ws = websocket wsclient.connecting = true wsclient.connected = false websocket.onmessage = event => { wsclient.lastMessageReceived = time.getUnixTime() const data = event.data const message = typeof data === 'string' ? JSON.parse(data) : data if (message && message.type === 'pong') { clearTimeout(pingTimeout) pingTimeout = setTimeout(sendPing, messageReconnectTimeout / 2) } wsclient.emit('message', [message, wsclient]) } /** * @param {any} error */ const onclose = error => { if (wsclient.ws !== null) { wsclient.ws = null wsclient.connecting = false if (wsclient.connected) { wsclient.connected = false wsclient.emit('disconnect', [{ type: 'disconnect', error }, wsclient]) } else { wsclient.unsuccessfulReconnects++ } // Start with no reconnect timeout and increase timeout by // log10(wsUnsuccessfulReconnects). // The idea is to increase reconnect timeout slowly and have no reconnect // timeout at the beginning (log(1) = 0) setTimeout(setupWS, math.min(math.log10(wsclient.unsuccessfulReconnects + 1) * reconnectTimeoutBase, maxReconnectTimeout), wsclient) } clearTimeout(pingTimeout) } const sendPing = () => { if (wsclient.ws === websocket) { wsclient.send({ type: 'ping' }) } } websocket.onclose = () => onclose(null) websocket.onerror = error => onclose(error) websocket.onopen = () => { wsclient.lastMessageReceived = time.getUnixTime() wsclient.connecting = false wsclient.connected = true wsclient.unsuccessfulReconnects = 0 wsclient.emit('connect', [{ type: 'connect' }, wsclient]) // set ping pingTimeout = setTimeout(sendPing, messageReconnectTimeout / 2) } } } /** * @deprecated * @extends Observable */ export class WebsocketClient extends Observable { /** * @param {string} url * @param {object} opts * @param {'arraybuffer' | 'blob' | null} [opts.binaryType] Set `ws.binaryType` */ constructor (url, { binaryType } = {}) { super() this.url = url /** * @type {WebSocket?} */ this.ws = null this.binaryType = binaryType || null this.connected = false this.connecting = false this.unsuccessfulReconnects = 0 this.lastMessageReceived = 0 /** * Whether to connect to other peers or not * @type {boolean} */ this.shouldConnect = true this._checkInterval = setInterval(() => { if (this.connected && messageReconnectTimeout < time.getUnixTime() - this.lastMessageReceived) { // no message received in a long time - not even your own awareness // updates (which are updated every 15 seconds) /** @type {WebSocket} */ (this.ws).close() } }, messageReconnectTimeout / 2) setupWS(this) } /** * @param {any} message */ send (message) { if (this.ws) { this.ws.send(JSON.stringify(message)) } } destroy () { clearInterval(this._checkInterval) this.disconnect() super.destroy() } disconnect () { this.shouldConnect = false if (this.ws !== null) { this.ws.close() } } connect () { this.shouldConnect = true if (!this.connected && this.ws === null) { setupWS(this) } } }