@twilio/voice-sdk
Version:
Twilio's JavaScript Voice SDK
450 lines • 33.8 kB
JavaScript
import { EventEmitter } from 'events';
import Backoff from './backoff';
import { SignalingErrors } from './errors';
import Log from './log';
const WebSocket = globalThis.WebSocket;
const CONNECT_SUCCESS_TIMEOUT = 10000;
const CONNECT_TIMEOUT = 5000;
const HEARTBEAT_TIMEOUT = 15000;
const MAX_PREFERRED_DURATION = 15000;
const MAX_PRIMARY_DURATION = Infinity;
const MAX_PREFERRED_DELAY = 1000;
const MAX_PRIMARY_DELAY = 20000;
/**
* All possible states of WSTransport.
*/
export var WSTransportState;
(function (WSTransportState) {
/**
* The WebSocket is not open but is trying to connect.
*/
WSTransportState["Connecting"] = "connecting";
/**
* The WebSocket is not open and is not trying to connect.
*/
WSTransportState["Closed"] = "closed";
/**
* The underlying WebSocket is open and active.
*/
WSTransportState["Open"] = "open";
})(WSTransportState || (WSTransportState = {}));
/**
* WebSocket Transport
*/
class WSTransport extends EventEmitter {
/**
* @constructor
* @param uris - List of URI of the endpoints to connect to.
* @param [options] - Constructor options.
*/
constructor(uris, options = {}) {
super();
/**
* The current state of the WSTransport.
*/
this.state = WSTransportState.Closed;
/**
* Start timestamp values for backoffs.
*/
this._backoffStartTime = {
preferred: null,
primary: null,
};
/**
* The URI that the transport is connecting or connected to. The value of this
* property is `null` if a connection attempt has not been made yet.
*/
this._connectedUri = null;
/**
* An instance of Logger to use.
*/
this._log = new Log('WSTransport');
/**
* Whether we should attempt to fallback if we receive an applicable error
* when trying to connect to a signaling endpoint.
*/
this._shouldFallback = false;
/**
* The current uri index that the transport is connected to.
*/
this._uriIndex = 0;
/**
* Move the uri index to the next index
* If the index is at the end, the index goes back to the first one.
*/
this._moveUriIndex = () => {
this._uriIndex++;
if (this._uriIndex >= this._uris.length) {
this._uriIndex = 0;
}
};
/**
* Called in response to WebSocket#close event.
*/
this._onSocketClose = (event) => {
this._log.error(`Received websocket close event code: ${event.code}. Reason: ${event.reason}`);
// 1006: Abnormal close. When the server is unreacheable
// 1015: TLS Handshake error
if (event.code === 1006 || event.code === 1015) {
this.emit('error', {
code: 31005,
message: event.reason ||
'Websocket connection to Twilio\'s signaling servers were ' +
'unexpectedly ended. If this is happening consistently, there may ' +
'be an issue resolving the hostname provided. If a region or an ' +
'edge is being specified in Device setup, ensure it is valid.',
twilioError: new SignalingErrors.ConnectionError(),
});
const wasConnected = (
// Only in Safari and certain Firefox versions, on network interruption, websocket drops right away with 1006
// Let's check current state if it's open, meaning we should not fallback
// because we're coming from a previously connected session
this.state === WSTransportState.Open ||
// But on other browsers, websocket doesn't drop
// but our heartbeat catches it, setting the internal state to "Connecting".
// With this, we should check the previous state instead.
this._previousState === WSTransportState.Open);
// Only fallback if this is not the first error
// and if we were not connected previously
if (this._shouldFallback || !wasConnected) {
this._moveUriIndex();
}
this._shouldFallback = true;
}
this._closeSocket();
};
/**
* Called in response to WebSocket#error event.
*/
this._onSocketError = (err) => {
this._log.error(`WebSocket received error: ${err.message}`);
this.emit('error', {
code: 31000,
message: err.message || 'WSTransport socket error',
twilioError: new SignalingErrors.ConnectionDisconnected(),
});
};
/**
* Called in response to WebSocket#message event.
*/
this._onSocketMessage = (message) => {
// Clear heartbeat timeout on any incoming message, as they
// all indicate an active connection.
this._setHeartbeatTimeout();
// Filter and respond to heartbeats
if (this._socket && message.data === '\n') {
this._socket.send('\n');
this._log.debug('heartbeat');
return;
}
if (message && typeof message.data === 'string') {
this._log.debug(`Received: ${message.data}`);
}
this.emit('message', message);
};
/**
* Called in response to WebSocket#open event.
*/
this._onSocketOpen = () => {
this._log.info('WebSocket opened successfully.');
this._timeOpened = Date.now();
this._shouldFallback = false;
this._setState(WSTransportState.Open);
clearTimeout(this._connectTimeout);
this._resetBackoffs();
this._setHeartbeatTimeout();
this.emit('open');
};
this._options = Object.assign(Object.assign({}, WSTransport.defaultConstructorOptions), options);
this._uris = uris;
this._backoff = this._setupBackoffs();
}
/**
* Close the WebSocket, and don't try to reconnect.
*/
close() {
this._log.info('WSTransport.close() called...');
this._close();
}
/**
* Attempt to open a WebSocket connection.
*/
open() {
this._log.info('WSTransport.open() called...');
if (this._socket &&
(this._socket.readyState === WebSocket.CONNECTING ||
this._socket.readyState === WebSocket.OPEN)) {
this._log.info('WebSocket already open.');
return;
}
if (this._preferredUri) {
this._connect(this._preferredUri);
}
else {
this._connect(this._uris[this._uriIndex]);
}
}
/**
* Send a message through the WebSocket connection.
* @param message - A message to send to the endpoint.
* @returns Whether the message was sent.
*/
send(message) {
this._log.debug(`Sending: ${message}`);
// We can't send the message if the WebSocket isn't open
if (!this._socket || this._socket.readyState !== WebSocket.OPEN) {
this._log.debug('Cannot send message. WebSocket is not open.');
return false;
}
try {
this._socket.send(message);
}
catch (e) {
// Some unknown error occurred. Reset the socket to get a fresh session.
this._log.error('Error while sending message:', e.message);
this._closeSocket();
return false;
}
return true;
}
/**
* Update the preferred URI to connect to. Useful for Call signaling
* reconnection, which requires connecting on the same edge. If `null` is
* passed, the preferred URI is unset and the original `uris` array and
* `uriIndex` is used to determine the signaling URI to connect to.
* @param uri
*/
updatePreferredURI(uri) {
this._preferredUri = uri;
}
/**
* Update acceptable URIs to reconnect to. Resets the URI index to 0.
*/
updateURIs(uris) {
if (typeof uris === 'string') {
uris = [uris];
}
this._uris = uris;
this._uriIndex = 0;
}
/**
* Close the WebSocket, and don't try to reconnect.
*/
_close() {
this._setState(WSTransportState.Closed);
this._closeSocket();
}
/**
* Close the WebSocket and remove all event listeners.
*/
_closeSocket() {
clearTimeout(this._connectTimeout);
clearTimeout(this._heartbeatTimeout);
this._log.info('Closing and cleaning up WebSocket...');
if (!this._socket) {
this._log.info('No WebSocket to clean up.');
return;
}
this._socket.removeEventListener('close', this._onSocketClose);
this._socket.removeEventListener('error', this._onSocketError);
this._socket.removeEventListener('message', this._onSocketMessage);
this._socket.removeEventListener('open', this._onSocketOpen);
if (this._socket.readyState === WebSocket.CONNECTING ||
this._socket.readyState === WebSocket.OPEN) {
this._socket.close();
}
// Reset backoff counter if connection was open for long enough to be considered successful
if (this._timeOpened && Date.now() - this._timeOpened > CONNECT_SUCCESS_TIMEOUT) {
this._resetBackoffs();
}
if (this.state !== WSTransportState.Closed) {
this._performBackoff();
}
delete this._socket;
this.emit('close');
}
/**
* Attempt to connect to the endpoint via WebSocket.
* @param [uri] - URI string to connect to.
* @param [retryCount] - Retry number, if this is a retry. Undefined if
* first attempt, 1+ if a retry.
*/
_connect(uri, retryCount) {
this._log.info(typeof retryCount === 'number'
? `Attempting to reconnect (retry #${retryCount})...`
: 'Attempting to connect...');
this._closeSocket();
this._setState(WSTransportState.Connecting);
this._connectedUri = uri;
try {
this._socket = new this._options.WebSocket(this._connectedUri);
}
catch (e) {
this._log.error('Could not connect to endpoint:', e.message);
this._close();
this.emit('error', {
code: 31000,
message: e.message || `Could not connect to ${this._connectedUri}`,
twilioError: new SignalingErrors.ConnectionDisconnected(),
});
return;
}
this._socket.addEventListener('close', this._onSocketClose);
this._socket.addEventListener('error', this._onSocketError);
this._socket.addEventListener('message', this._onSocketMessage);
this._socket.addEventListener('open', this._onSocketOpen);
delete this._timeOpened;
this._connectTimeout = setTimeout(() => {
this._log.info('WebSocket connection attempt timed out.');
this._moveUriIndex();
this._closeSocket();
}, this._options.connectTimeoutMs);
}
/**
* Perform a backoff. If a preferred URI is set (not null), then backoff
* using the preferred mechanism. Otherwise, use the primary mechanism.
*/
_performBackoff() {
if (this._preferredUri) {
this._log.info('Preferred URI set; backing off.');
this._backoff.preferred.backoff();
}
else {
this._log.info('Preferred URI not set; backing off.');
this._backoff.primary.backoff();
}
}
/**
* Reset both primary and preferred backoff mechanisms.
*/
_resetBackoffs() {
this._backoff.preferred.reset();
this._backoff.primary.reset();
this._backoffStartTime.preferred = null;
this._backoffStartTime.primary = null;
}
/**
* Set a timeout to reconnect after HEARTBEAT_TIMEOUT milliseconds
* have passed without receiving a message over the WebSocket.
*/
_setHeartbeatTimeout() {
clearTimeout(this._heartbeatTimeout);
this._heartbeatTimeout = setTimeout(() => {
this._log.info(`No messages received in ${HEARTBEAT_TIMEOUT / 1000} seconds. Reconnecting...`);
this._shouldFallback = true;
this._closeSocket();
}, HEARTBEAT_TIMEOUT);
}
/**
* Set the current and previous state
*/
_setState(state) {
this._previousState = this.state;
this.state = state;
}
/**
* Set up the primary and preferred backoff mechanisms.
*/
_setupBackoffs() {
const preferredBackoffConfig = {
factor: 2.0,
jitter: 0.40,
max: this._options.maxPreferredDelayMs,
min: 100,
};
this._log.info('Initializing preferred transport backoff using config: ', preferredBackoffConfig);
const preferredBackoff = new Backoff(preferredBackoffConfig);
preferredBackoff.on('backoff', (attempt, delay) => {
if (this.state === WSTransportState.Closed) {
this._log.info('Preferred backoff initiated but transport state is closed; not attempting a connection.');
return;
}
this._log.info(`Will attempt to reconnect Websocket to preferred URI in ${delay}ms`);
if (attempt === 0) {
this._backoffStartTime.preferred = Date.now();
this._log.info(`Preferred backoff start; ${this._backoffStartTime.preferred}`);
}
});
preferredBackoff.on('ready', (attempt, _delay) => {
if (this.state === WSTransportState.Closed) {
this._log.info('Preferred backoff ready but transport state is closed; not attempting a connection.');
return;
}
if (this._backoffStartTime.preferred === null) {
this._log.info('Preferred backoff start time invalid; not attempting a connection.');
return;
}
if (Date.now() - this._backoffStartTime.preferred > this._options.maxPreferredDurationMs) {
this._log.info('Max preferred backoff attempt time exceeded; falling back to primary backoff.');
this._preferredUri = null;
this._backoff.primary.backoff();
return;
}
if (typeof this._preferredUri !== 'string') {
this._log.info('Preferred URI cleared; falling back to primary backoff.');
this._preferredUri = null;
this._backoff.primary.backoff();
return;
}
this._connect(this._preferredUri, attempt + 1);
});
const primaryBackoffConfig = {
factor: 2.0,
jitter: 0.40,
max: this._options.maxPrimaryDelayMs,
// We only want a random initial delay if there are any fallback edges
// Initial delay between 1s and 5s both inclusive
min: this._uris && this._uris.length > 1
? Math.floor(Math.random() * (5000 - 1000 + 1)) + 1000
: 100,
};
this._log.info('Initializing primary transport backoff using config: ', primaryBackoffConfig);
const primaryBackoff = new Backoff(primaryBackoffConfig);
primaryBackoff.on('backoff', (attempt, delay) => {
if (this.state === WSTransportState.Closed) {
this._log.info('Primary backoff initiated but transport state is closed; not attempting a connection.');
return;
}
this._log.info(`Will attempt to reconnect WebSocket in ${delay}ms`);
if (attempt === 0) {
this._backoffStartTime.primary = Date.now();
this._log.info(`Primary backoff start; ${this._backoffStartTime.primary}`);
}
});
primaryBackoff.on('ready', (attempt, _delay) => {
if (this.state === WSTransportState.Closed) {
this._log.info('Primary backoff ready but transport state is closed; not attempting a connection.');
return;
}
if (this._backoffStartTime.primary === null) {
this._log.info('Primary backoff start time invalid; not attempting a connection.');
return;
}
if (Date.now() - this._backoffStartTime.primary > this._options.maxPrimaryDurationMs) {
this._log.info('Max primary backoff attempt time exceeded; not attempting a connection.');
return;
}
this._connect(this._uris[this._uriIndex], attempt + 1);
});
return {
preferred: preferredBackoff,
primary: primaryBackoff,
};
}
/**
* The uri the transport is currently connected to
*/
get uri() {
return this._connectedUri;
}
}
WSTransport.defaultConstructorOptions = {
WebSocket,
connectTimeoutMs: CONNECT_TIMEOUT,
maxPreferredDelayMs: MAX_PREFERRED_DELAY,
maxPreferredDurationMs: MAX_PREFERRED_DURATION,
maxPrimaryDelayMs: MAX_PRIMARY_DELAY,
maxPrimaryDurationMs: MAX_PRIMARY_DURATION,
};
export default WSTransport;
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid3N0cmFuc3BvcnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9saWIvdHdpbGlvL3dzdHJhbnNwb3J0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxRQUFRLENBQUM7QUFDdEMsT0FBTyxPQUFPLE1BQU0sV0FBVyxDQUFDO0FBQ2hDLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSxVQUFVLENBQUM7QUFDM0MsT0FBTyxHQUFHLE1BQU0sT0FBTyxDQUFDO0FBRXhCLE1BQU0sU0FBUyxHQUFHLFVBQVUsQ0FBQyxTQUFTLENBQUM7QUFFdkMsTUFBTSx1QkFBdUIsR0FBRyxLQUFLLENBQUM7QUFDdEMsTUFBTSxlQUFlLEdBQUcsSUFBSSxDQUFDO0FBQzdCLE1BQU0saUJBQWlCLEdBQUcsS0FBSyxDQUFDO0FBQ2hDLE1BQU0sc0JBQXNCLEdBQUcsS0FBSyxDQUFDO0FBQ3JDLE1BQU0sb0JBQW9CLEdBQUcsUUFBUSxDQUFDO0FBQ3RDLE1BQU0sbUJBQW1CLEdBQUcsSUFBSSxDQUFDO0FBQ2pDLE1BQU0saUJBQWlCLEdBQUcsS0FBSyxDQUFDO0FBUWhDOztHQUVHO0FBQ0gsTUFBTSxDQUFOLElBQVksZ0JBZVg7QUFmRCxXQUFZLGdCQUFnQjtJQUMxQjs7T0FFRztJQUNILDZDQUF5QixDQUFBO0lBRXpCOztPQUVHO0lBQ0gscUNBQWlCLENBQUE7SUFFakI7O09BRUc7SUFDSCxpQ0FBYSxDQUFBO0FBQ2YsQ0FBQyxFQWZXLGdCQUFnQixLQUFoQixnQkFBZ0IsUUFlM0I7QUEwQ0Q7O0dBRUc7QUFDSCxNQUFxQixXQUFZLFNBQVEsWUFBWTtJQXdHbkQ7Ozs7T0FJRztJQUNILFlBQVksSUFBYyxFQUFFLFVBQTBDLEVBQUc7UUFDdkUsS0FBSyxFQUFFLENBQUM7UUFwR1Y7O1dBRUc7UUFDSCxVQUFLLEdBQXFCLGdCQUFnQixDQUFDLE1BQU0sQ0FBQztRQVVsRDs7V0FFRztRQUNLLHNCQUFpQixHQUdy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