UNPKG

4.28 kBJavaScriptView Raw
1/* eslint-env browser */
2
3/**
4 * Tiny websocket connection handler.
5 *
6 * Implements exponential backoff reconnects, ping/pong, and a nice event system using [lib0/observable].
7 *
8 * @module websocket
9 */
10
11import { Observable } from './observable.js'
12import * as time from './time.js'
13import * as math from './math.js'
14
15const reconnectTimeoutBase = 1200
16const maxReconnectTimeout = 2500
17// @todo - this should depend on awareness.outdatedTime
18const messageReconnectTimeout = 30000
19
20/**
21 * @param {WebsocketClient} wsclient
22 */
23const setupWS = (wsclient) => {
24 if (wsclient.shouldConnect && wsclient.ws === null) {
25 const websocket = new WebSocket(wsclient.url)
26 const binaryType = wsclient.binaryType
27 /**
28 * @type {any}
29 */
30 let pingTimeout = null
31 if (binaryType) {
32 websocket.binaryType = binaryType
33 }
34 wsclient.ws = websocket
35 wsclient.connecting = true
36 wsclient.connected = false
37 websocket.onmessage = event => {
38 wsclient.lastMessageReceived = time.getUnixTime()
39 const data = event.data
40 const message = typeof data === 'string' ? JSON.parse(data) : data
41 if (message && message.type === 'pong') {
42 clearTimeout(pingTimeout)
43 pingTimeout = setTimeout(sendPing, messageReconnectTimeout / 2)
44 }
45 wsclient.emit('message', [message, wsclient])
46 }
47 /**
48 * @param {any} error
49 */
50 const onclose = error => {
51 if (wsclient.ws !== null) {
52 wsclient.ws = null
53 wsclient.connecting = false
54 if (wsclient.connected) {
55 wsclient.connected = false
56 wsclient.emit('disconnect', [{ type: 'disconnect', error }, wsclient])
57 } else {
58 wsclient.unsuccessfulReconnects++
59 }
60 // Start with no reconnect timeout and increase timeout by
61 // log10(wsUnsuccessfulReconnects).
62 // The idea is to increase reconnect timeout slowly and have no reconnect
63 // timeout at the beginning (log(1) = 0)
64 setTimeout(setupWS, math.min(math.log10(wsclient.unsuccessfulReconnects + 1) * reconnectTimeoutBase, maxReconnectTimeout), wsclient)
65 }
66 clearTimeout(pingTimeout)
67 }
68 const sendPing = () => {
69 if (wsclient.ws === websocket) {
70 wsclient.send({
71 type: 'ping'
72 })
73 }
74 }
75 websocket.onclose = () => onclose(null)
76 websocket.onerror = error => onclose(error)
77 websocket.onopen = () => {
78 wsclient.lastMessageReceived = time.getUnixTime()
79 wsclient.connecting = false
80 wsclient.connected = true
81 wsclient.unsuccessfulReconnects = 0
82 wsclient.emit('connect', [{ type: 'connect' }, wsclient])
83 // set ping
84 pingTimeout = setTimeout(sendPing, messageReconnectTimeout / 2)
85 }
86 }
87}
88
89/**
90 * @extends Observable<string>
91 */
92export class WebsocketClient extends Observable {
93 /**
94 * @param {string} url
95 * @param {object} [opts]
96 * @param {'arraybuffer' | 'blob' | null} [opts.binaryType] Set `ws.binaryType`
97 */
98 constructor (url, { binaryType } = {}) {
99 super()
100 this.url = url
101 /**
102 * @type {WebSocket?}
103 */
104 this.ws = null
105 this.binaryType = binaryType || null
106 this.connected = false
107 this.connecting = false
108 this.unsuccessfulReconnects = 0
109 this.lastMessageReceived = 0
110 /**
111 * Whether to connect to other peers or not
112 * @type {boolean}
113 */
114 this.shouldConnect = true
115 this._checkInterval = setInterval(() => {
116 if (this.connected && messageReconnectTimeout < time.getUnixTime() - this.lastMessageReceived) {
117 // no message received in a long time - not even your own awareness
118 // updates (which are updated every 15 seconds)
119 /** @type {WebSocket} */ (this.ws).close()
120 }
121 }, messageReconnectTimeout / 2)
122 setupWS(this)
123 }
124
125 /**
126 * @param {any} message
127 */
128 send (message) {
129 if (this.ws) {
130 this.ws.send(JSON.stringify(message))
131 }
132 }
133
134 destroy () {
135 clearInterval(this._checkInterval)
136 this.disconnect()
137 super.destroy()
138 }
139
140 disconnect () {
141 this.shouldConnect = false
142 if (this.ws !== null) {
143 this.ws.close()
144 }
145 }
146
147 connect () {
148 this.shouldConnect = true
149 if (!this.connected && this.ws === null) {
150 setupWS(this)
151 }
152 }
153}