robust-websocket
Version:
A robust reconnecting WebSocket client for the browser
245 lines (210 loc) • 7.37 kB
JavaScript
(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));