UNPKG

robust-websocket

Version:

A robust reconnecting WebSocket client for the browser

245 lines (210 loc) 7.37 kB
(function(factory, global) { if (typeof define === 'function' && define.amd) { define(function() { return factory(global, navigator) }) } else if (typeof exports === 'object' && typeof module === 'object') { module.exports = factory(global, navigator) } else { // mock the navigator object when under test since `navigator.onLine` is read only global.RobustWebSocket = factory(global, typeof Mocha !== 'undefined' ? Mocha : navigator) } })(function(global, navigator) { var RobustWebSocket = function(url, protocols, userOptions) { var realWs = { close: function() {} }, connectTimeout, self = this, attempts = 0, reconnects = -1, reconnectWhenOnlineAgain = false, explicitlyClosed = false, pendingReconnect, opts = Object.assign({}, RobustWebSocket.defaultOptions, typeof userOptions === 'function' ? { shouldReconnect: userOptions } : userOptions ) if (typeof opts.timeout !== 'number') { throw new Error('timeout must be the number of milliseconds to timeout a connection attempt') } if (typeof opts.shouldReconnect !== 'function') { throw new Error('shouldReconnect must be a function that returns the number of milliseconds to wait for a reconnect attempt, or null or undefined to not reconnect.') } ['bufferedAmount', 'url', 'readyState', 'protocol', 'extensions'].forEach(function(readOnlyProp) { Object.defineProperty(self, readOnlyProp, { get: function() { return realWs[readOnlyProp] } }) }) function clearPendingReconnectIfNeeded() { if (pendingReconnect) { clearTimeout(pendingReconnect) pendingReconnect = null } } var ononline = function(event) { if (reconnectWhenOnlineAgain) { clearPendingReconnectIfNeeded() reconnect(event) } }, onoffline = function() { reconnectWhenOnlineAgain = true realWs.close(1000) }, connectivityEventsAttached = false function detachConnectivityEvents() { if (connectivityEventsAttached) { global.removeEventListener('online', ononline) global.removeEventListener('offline', onoffline) connectivityEventsAttached = false } } function attachConnectivityEvents() { if (!connectivityEventsAttached) { global.addEventListener('online', ononline) global.addEventListener('offline', onoffline) connectivityEventsAttached = true } } self.send = function() { return realWs.send.apply(realWs, arguments) } self.close = function(code, reason) { if (typeof code !== 'number') { reason = code code = 1000 } clearPendingReconnectIfNeeded() reconnectWhenOnlineAgain = false explicitlyClosed = true detachConnectivityEvents() return realWs.close(code, reason) } self.open = function() { if (realWs.readyState !== WebSocket.OPEN && realWs.readyState !== WebSocket.CONNECTING) { clearPendingReconnectIfNeeded() reconnectWhenOnlineAgain = false explicitlyClosed = false newWebSocket() } } function reconnect(event) { if ((!opts.shouldReconnect.handle1000 && event.code === 1000) || explicitlyClosed) { attempts = 0 return } if (navigator.onLine === false) { reconnectWhenOnlineAgain = true return } var delay = opts.shouldReconnect(event, self) if (typeof delay === 'number') { pendingReconnect = setTimeout(newWebSocket, delay) } } Object.defineProperty(self, 'listeners', { value: { open: [function(event) { if (connectTimeout) { clearTimeout(connectTimeout) connectTimeout = null } event.reconnects = ++reconnects event.attempts = attempts attempts = 0 reconnectWhenOnlineAgain = false }], close: [reconnect] } }) Object.defineProperty(self, 'attempts', { get: function() { return attempts }, enumerable: true }) Object.defineProperty(self, 'reconnects', { get: function() { return reconnects }, enumerable: true }) function newWebSocket() { var newUrl = (typeof url === 'function' ? url(self) : url); pendingReconnect = null realWs = new WebSocket(newUrl, protocols || undefined) realWs.binaryType = self.binaryType attempts++ self.dispatchEvent(Object.assign(new CustomEvent('connecting'), { attempts: attempts, reconnects: reconnects })) connectTimeout = setTimeout(function() { connectTimeout = null detachConnectivityEvents() self.dispatchEvent(Object.assign(new CustomEvent('timeout'), { attempts: attempts, reconnects: reconnects })) }, opts.timeout) ;['open', 'close', 'message', 'error'].forEach(function(stdEvent) { realWs.addEventListener(stdEvent, function(event) { self.dispatchEvent(event) var cb = self['on' + stdEvent] if (typeof cb === 'function') { return cb.apply(self, arguments) } }) }) if (!opts.ignoreConnectivityEvents) { attachConnectivityEvents() } } if (opts.automaticOpen) { newWebSocket() } } RobustWebSocket.defaultOptions = { // the time to wait before a successful connection // before the attempt is considered to have timed out timeout: 4000, // Given a CloseEvent or OnlineEvent and the RobustWebSocket state, // should a reconnect be attempted? Return the number of milliseconds to wait // to reconnect (or null or undefined to not), rather than true or false shouldReconnect: function(event, ws) { if (event.code === 1008 || event.code === 1011) return return [0, 3000, 10000][ws.attempts] }, // Flag to control whether attachement to navigator online/offline events // should be disabled. ignoreConnectivityEvents: false, // Create and connect the WebSocket when the instance is instantiated. // Defaults to true to match standard WebSocket behavior automaticOpen: true } RobustWebSocket.prototype.binaryType = 'blob' // Taken from MDN https://developer.mozilla.org/en-US/docs/Web/API/EventTarget RobustWebSocket.prototype.addEventListener = function(type, callback) { if (!(type in this.listeners)) { this.listeners[type] = [] } this.listeners[type].push(callback) } RobustWebSocket.prototype.removeEventListener = function(type, callback) { if (!(type in this.listeners)) { return } var stack = this.listeners[type] for (var i = 0, l = stack.length; i < l; i++) { if (stack[i] === callback) { stack.splice(i, 1) return } } } RobustWebSocket.prototype.dispatchEvent = function(event) { if (!(event.type in this.listeners)) { return } var stack = this.listeners[event.type] for (var i = 0, l = stack.length; i < l; i++) { stack[i].call(this, event) } } return RobustWebSocket }, typeof window != 'undefined' ? window : (typeof global != 'undefined' ? global : this));