reconnecting-websocket
Version:
Reconnecting WebSocket
592 lines (579 loc) • 24.3 kB
JavaScript
define(function () { 'use strict';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
function __values(o) {
var m = typeof Symbol === "function" && o[Symbol.iterator], i = 0;
if (m) return m.call(o);
return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
}
function __read(o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
}
function __spread() {
for (var ar = [], i = 0; i < arguments.length; i++)
ar = ar.concat(__read(arguments[i]));
return ar;
}
var Event = /** @class */ (function () {
function Event(type, target) {
this.target = target;
this.type = type;
}
return Event;
}());
var ErrorEvent = /** @class */ (function (_super) {
__extends(ErrorEvent, _super);
function ErrorEvent(error, target) {
var _this = _super.call(this, 'error', target) || this;
_this.message = error.message;
_this.error = error;
return _this;
}
return ErrorEvent;
}(Event));
var CloseEvent = /** @class */ (function (_super) {
__extends(CloseEvent, _super);
function CloseEvent(code, reason, target) {
if (code === void 0) { code = 1000; }
if (reason === void 0) { reason = ''; }
var _this = _super.call(this, 'close', target) || this;
_this.wasClean = true;
_this.code = code;
_this.reason = reason;
return _this;
}
return CloseEvent;
}(Event));
/*!
* Reconnecting WebSocket
* by Pedro Ladaria <pedro.ladaria@gmail.com>
* https://github.com/pladaria/reconnecting-websocket
* License MIT
*/
var getGlobalWebSocket = function () {
if (typeof WebSocket !== 'undefined') {
// @ts-ignore
return WebSocket;
}
};
/**
* Returns true if given argument looks like a WebSocket class
*/
var isWebSocket = function (w) { return typeof w !== 'undefined' && !!w && w.CLOSING === 2; };
var DEFAULT = {
maxReconnectionDelay: 10000,
minReconnectionDelay: 1000 + Math.random() * 4000,
minUptime: 5000,
reconnectionDelayGrowFactor: 1.3,
connectionTimeout: 4000,
maxRetries: Infinity,
maxEnqueuedMessages: Infinity,
startClosed: false,
debug: false,
};
var ReconnectingWebSocket = /** @class */ (function () {
function ReconnectingWebSocket(url, protocols, options) {
var _this = this;
if (options === void 0) { options = {}; }
this._listeners = {
error: [],
message: [],
open: [],
close: [],
};
this._retryCount = -1;
this._shouldReconnect = true;
this._connectLock = false;
this._binaryType = 'blob';
this._closeCalled = false;
this._messageQueue = [];
/**
* An event listener to be called when the WebSocket connection's readyState changes to CLOSED
*/
this.onclose = null;
/**
* An event listener to be called when an error occurs
*/
this.onerror = null;
/**
* An event listener to be called when a message is received from the server
*/
this.onmessage = null;
/**
* An event listener to be called when the WebSocket connection's readyState changes to OPEN;
* this indicates that the connection is ready to send and receive data
*/
this.onopen = null;
this._handleOpen = function (event) {
_this._debug('open event');
var _a = _this._options.minUptime, minUptime = _a === void 0 ? DEFAULT.minUptime : _a;
clearTimeout(_this._connectTimeout);
_this._uptimeTimeout = setTimeout(function () { return _this._acceptOpen(); }, minUptime);
_this._ws.binaryType = _this._binaryType;
// send enqueued messages (messages sent before websocket open event)
_this._messageQueue.forEach(function (message) { return _this._ws.send(message); });
_this._messageQueue = [];
if (_this.onopen) {
_this.onopen(event);
}
_this._listeners.open.forEach(function (listener) { return _this._callEventListener(event, listener); });
};
this._handleMessage = function (event) {
_this._debug('message event');
if (_this.onmessage) {
_this.onmessage(event);
}
_this._listeners.message.forEach(function (listener) { return _this._callEventListener(event, listener); });
};
this._handleError = function (event) {
_this._debug('error event', event.message);
_this._disconnect(undefined, event.message === 'TIMEOUT' ? 'timeout' : undefined);
if (_this.onerror) {
_this.onerror(event);
}
_this._debug('exec error listeners');
_this._listeners.error.forEach(function (listener) { return _this._callEventListener(event, listener); });
_this._connect();
};
this._handleClose = function (event) {
_this._debug('close event');
_this._clearTimeouts();
if (_this._shouldReconnect) {
_this._connect();
}
if (_this.onclose) {
_this.onclose(event);
}
_this._listeners.close.forEach(function (listener) { return _this._callEventListener(event, listener); });
};
this._url = url;
this._protocols = protocols;
this._options = options;
if (this._options.startClosed) {
this._shouldReconnect = false;
}
this._connect();
}
Object.defineProperty(ReconnectingWebSocket, "CONNECTING", {
get: function () {
return 0;
},
enumerable: true,
configurable: true
});
Object.defineProperty(ReconnectingWebSocket, "OPEN", {
get: function () {
return 1;
},
enumerable: true,
configurable: true
});
Object.defineProperty(ReconnectingWebSocket, "CLOSING", {
get: function () {
return 2;
},
enumerable: true,
configurable: true
});
Object.defineProperty(ReconnectingWebSocket, "CLOSED", {
get: function () {
return 3;
},
enumerable: true,
configurable: true
});
Object.defineProperty(ReconnectingWebSocket.prototype, "CONNECTING", {
get: function () {
return ReconnectingWebSocket.CONNECTING;
},
enumerable: true,
configurable: true
});
Object.defineProperty(ReconnectingWebSocket.prototype, "OPEN", {
get: function () {
return ReconnectingWebSocket.OPEN;
},
enumerable: true,
configurable: true
});
Object.defineProperty(ReconnectingWebSocket.prototype, "CLOSING", {
get: function () {
return ReconnectingWebSocket.CLOSING;
},
enumerable: true,
configurable: true
});
Object.defineProperty(ReconnectingWebSocket.prototype, "CLOSED", {
get: function () {
return ReconnectingWebSocket.CLOSED;
},
enumerable: true,
configurable: true
});
Object.defineProperty(ReconnectingWebSocket.prototype, "binaryType", {
get: function () {
return this._ws ? this._ws.binaryType : this._binaryType;
},
set: function (value) {
this._binaryType = value;
if (this._ws) {
this._ws.binaryType = value;
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(ReconnectingWebSocket.prototype, "retryCount", {
/**
* Returns the number or connection retries
*/
get: function () {
return Math.max(this._retryCount, 0);
},
enumerable: true,
configurable: true
});
Object.defineProperty(ReconnectingWebSocket.prototype, "bufferedAmount", {
/**
* The number of bytes of data that have been queued using calls to send() but not yet
* transmitted to the network. This value resets to zero once all queued data has been sent.
* This value does not reset to zero when the connection is closed; if you keep calling send(),
* this will continue to climb. Read only
*/
get: function () {
var bytes = this._messageQueue.reduce(function (acc, message) {
if (typeof message === 'string') {
acc += message.length; // not byte size
}
else if (message instanceof Blob) {
acc += message.size;
}
else {
acc += message.byteLength;
}
return acc;
}, 0);
return bytes + (this._ws ? this._ws.bufferedAmount : 0);
},
enumerable: true,
configurable: true
});
Object.defineProperty(ReconnectingWebSocket.prototype, "extensions", {
/**
* The extensions selected by the server. This is currently only the empty string or a list of
* extensions as negotiated by the connection
*/
get: function () {
return this._ws ? this._ws.extensions : '';
},
enumerable: true,
configurable: true
});
Object.defineProperty(ReconnectingWebSocket.prototype, "protocol", {
/**
* A string indicating the name of the sub-protocol the server selected;
* this will be one of the strings specified in the protocols parameter when creating the
* WebSocket object
*/
get: function () {
return this._ws ? this._ws.protocol : '';
},
enumerable: true,
configurable: true
});
Object.defineProperty(ReconnectingWebSocket.prototype, "readyState", {
/**
* The current state of the connection; this is one of the Ready state constants
*/
get: function () {
if (this._ws) {
return this._ws.readyState;
}
return this._options.startClosed
? ReconnectingWebSocket.CLOSED
: ReconnectingWebSocket.CONNECTING;
},
enumerable: true,
configurable: true
});
Object.defineProperty(ReconnectingWebSocket.prototype, "url", {
/**
* The URL as resolved by the constructor
*/
get: function () {
return this._ws ? this._ws.url : '';
},
enumerable: true,
configurable: true
});
/**
* Closes the WebSocket connection or connection attempt, if any. If the connection is already
* CLOSED, this method does nothing
*/
ReconnectingWebSocket.prototype.close = function (code, reason) {
if (code === void 0) { code = 1000; }
this._closeCalled = true;
this._shouldReconnect = false;
this._clearTimeouts();
if (!this._ws) {
this._debug('close enqueued: no ws instance');
return;
}
if (this._ws.readyState === this.CLOSED) {
this._debug('close: already closed');
return;
}
this._ws.close(code, reason);
};
/**
* Closes the WebSocket connection or connection attempt and connects again.
* Resets retry counter;
*/
ReconnectingWebSocket.prototype.reconnect = function (code, reason) {
this._shouldReconnect = true;
this._closeCalled = false;
this._retryCount = -1;
if (!this._ws || this._ws.readyState === this.CLOSED) {
this._connect();
}
else {
this._disconnect(code, reason);
this._connect();
}
};
/**
* Enqueue specified data to be transmitted to the server over the WebSocket connection
*/
ReconnectingWebSocket.prototype.send = function (data) {
if (this._ws && this._ws.readyState === this.OPEN) {
this._debug('send', data);
this._ws.send(data);
}
else {
var _a = this._options.maxEnqueuedMessages, maxEnqueuedMessages = _a === void 0 ? DEFAULT.maxEnqueuedMessages : _a;
if (this._messageQueue.length < maxEnqueuedMessages) {
this._debug('enqueue', data);
this._messageQueue.push(data);
}
}
};
/**
* Register an event handler of a specific event type
*/
ReconnectingWebSocket.prototype.addEventListener = function (type, listener) {
if (this._listeners[type]) {
// @ts-ignore
this._listeners[type].push(listener);
}
};
ReconnectingWebSocket.prototype.dispatchEvent = function (event) {
var e_1, _a;
var listeners = this._listeners[event.type];
if (listeners) {
try {
for (var listeners_1 = __values(listeners), listeners_1_1 = listeners_1.next(); !listeners_1_1.done; listeners_1_1 = listeners_1.next()) {
var listener = listeners_1_1.value;
this._callEventListener(event, listener);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (listeners_1_1 && !listeners_1_1.done && (_a = listeners_1.return)) _a.call(listeners_1);
}
finally { if (e_1) throw e_1.error; }
}
}
return true;
};
/**
* Removes an event listener
*/
ReconnectingWebSocket.prototype.removeEventListener = function (type, listener) {
if (this._listeners[type]) {
// @ts-ignore
this._listeners[type] = this._listeners[type].filter(function (l) { return l !== listener; });
}
};
ReconnectingWebSocket.prototype._debug = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
if (this._options.debug) {
// not using spread because compiled version uses Symbols
// tslint:disable-next-line
console.log.apply(console, __spread(['RWS>'], args));
}
};
ReconnectingWebSocket.prototype._getNextDelay = function () {
var _a = this._options, _b = _a.reconnectionDelayGrowFactor, reconnectionDelayGrowFactor = _b === void 0 ? DEFAULT.reconnectionDelayGrowFactor : _b, _c = _a.minReconnectionDelay, minReconnectionDelay = _c === void 0 ? DEFAULT.minReconnectionDelay : _c, _d = _a.maxReconnectionDelay, maxReconnectionDelay = _d === void 0 ? DEFAULT.maxReconnectionDelay : _d;
var delay = 0;
if (this._retryCount > 0) {
delay =
minReconnectionDelay * Math.pow(reconnectionDelayGrowFactor, this._retryCount - 1);
if (delay > maxReconnectionDelay) {
delay = maxReconnectionDelay;
}
}
this._debug('next delay', delay);
return delay;
};
ReconnectingWebSocket.prototype._wait = function () {
var _this = this;
return new Promise(function (resolve) {
setTimeout(resolve, _this._getNextDelay());
});
};
ReconnectingWebSocket.prototype._getNextUrl = function (urlProvider) {
if (typeof urlProvider === 'string') {
return Promise.resolve(urlProvider);
}
if (typeof urlProvider === 'function') {
var url = urlProvider();
if (typeof url === 'string') {
return Promise.resolve(url);
}
if (!!url.then) {
return url;
}
}
throw Error('Invalid URL');
};
ReconnectingWebSocket.prototype._connect = function () {
var _this = this;
if (this._connectLock || !this._shouldReconnect) {
return;
}
this._connectLock = true;
var _a = this._options, _b = _a.maxRetries, maxRetries = _b === void 0 ? DEFAULT.maxRetries : _b, _c = _a.connectionTimeout, connectionTimeout = _c === void 0 ? DEFAULT.connectionTimeout : _c, _d = _a.WebSocket, WebSocket = _d === void 0 ? getGlobalWebSocket() : _d;
if (this._retryCount >= maxRetries) {
this._debug('max retries reached', this._retryCount, '>=', maxRetries);
return;
}
this._retryCount++;
this._debug('connect', this._retryCount);
this._removeListeners();
if (!isWebSocket(WebSocket)) {
throw Error('No valid WebSocket class provided');
}
this._wait()
.then(function () { return _this._getNextUrl(_this._url); })
.then(function (url) {
// close could be called before creating the ws
if (_this._closeCalled) {
return;
}
_this._debug('connect', { url: url, protocols: _this._protocols });
_this._ws = _this._protocols
? new WebSocket(url, _this._protocols)
: new WebSocket(url);
_this._ws.binaryType = _this._binaryType;
_this._connectLock = false;
_this._addListeners();
_this._connectTimeout = setTimeout(function () { return _this._handleTimeout(); }, connectionTimeout);
});
};
ReconnectingWebSocket.prototype._handleTimeout = function () {
this._debug('timeout event');
this._handleError(new ErrorEvent(Error('TIMEOUT'), this));
};
ReconnectingWebSocket.prototype._disconnect = function (code, reason) {
if (code === void 0) { code = 1000; }
this._clearTimeouts();
if (!this._ws) {
return;
}
this._removeListeners();
try {
this._ws.close(code, reason);
this._handleClose(new CloseEvent(code, reason, this));
}
catch (error) {
// ignore
}
};
ReconnectingWebSocket.prototype._acceptOpen = function () {
this._debug('accept open');
this._retryCount = 0;
};
ReconnectingWebSocket.prototype._callEventListener = function (event, listener) {
if ('handleEvent' in listener) {
// @ts-ignore
listener.handleEvent(event);
}
else {
// @ts-ignore
listener(event);
}
};
ReconnectingWebSocket.prototype._removeListeners = function () {
if (!this._ws) {
return;
}
this._debug('removeListeners');
this._ws.removeEventListener('open', this._handleOpen);
this._ws.removeEventListener('close', this._handleClose);
this._ws.removeEventListener('message', this._handleMessage);
// @ts-ignore
this._ws.removeEventListener('error', this._handleError);
};
ReconnectingWebSocket.prototype._addListeners = function () {
if (!this._ws) {
return;
}
this._debug('addListeners');
this._ws.addEventListener('open', this._handleOpen);
this._ws.addEventListener('close', this._handleClose);
this._ws.addEventListener('message', this._handleMessage);
// @ts-ignore
this._ws.addEventListener('error', this._handleError);
};
ReconnectingWebSocket.prototype._clearTimeouts = function () {
clearTimeout(this._connectTimeout);
clearTimeout(this._uptimeTimeout);
};
return ReconnectingWebSocket;
}());
return ReconnectingWebSocket;
});