UNPKG

@twilio/plugin-microvisor

Version:

Interact with your Twilio Microvisor devices

166 lines (165 loc) 6.26 kB
"use strict"; const EventEmitter = require('events'); const WebSocket = require('ws'); const { TwilioCliError } = require('@twilio/cli-core').services.error; const DEVICE_LOGGING_REFRESH_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes const INITIAL_RECONNECT_RETRY_DELAY_MS = 1000; const MAX_RECONNECT_RETRY_ATTEMPTS = 8; const PING_INTERVAL_MS = 300 * 1000; const PING_TIMEOUT_MS = 60 * 1000; class DeviceLogClient extends EventEmitter { constructor(opts) { super(); this.twilioClient = opts.twilioClient; this.tokenRequestOpts = opts.tokenRequestOpts; this.enableLoggingRequestOpts = opts.enableLoggingRequestOpts; this.websocketUrl = opts.websocketUrl; this.reconnectRetryCount = 0; this.connectionEstablished = false; this.connect(); } async connect() { var token = await this.getToken(); var options = { headers: { Authorization: `Bearer ${token}` } }; if (process.env.HTTP_PROXY) { const HttpsProxyAgent = require('https-proxy-agent'); options.agent = new HttpsProxyAgent(process.env.HTTP_PROXY); } this.ws = new WebSocket(this.websocketUrl, options); this.ws.on('open', () => this.onOpen()); this.ws.on('unexpected-response', (req, res) => this.onUnexpectedResponse(req, res)); this.ws.on('error', err => this.onError(err)); } async reconnectWithBackoff() { this.emit('error', new Error('Connection error; attempting reconnect')); this.reconnectRetryCount++; if (this.reconnectRetryCount > MAX_RECONNECT_RETRY_ATTEMPTS) { this.emit('connectFailed', 'Maximum reconnect attempts exceeded. Unable to reconnect'); return; } if (this.reconnectRetryCount <= 1) { // first retry should be immediate return this.connect(); } var delay = INITIAL_RECONNECT_RETRY_DELAY_MS * (2 ** (this.reconnectRetryCount - 2)); await new Promise(resolve => { setTimeout(resolve, delay); }); return this.connect(); } async getToken() { try { var response = await this.twilioClient.request(this.tokenRequestOpts); return response.body.token; } catch (error) { if (error instanceof TwilioCliError && error.exitCode === 20404) { this.emit('error', new TwilioCliError('Unknown device sid', error.exitCode, error.data)); } else { this.emit('error', error); } } } async enableDeviceLogging() { try { await this.twilioClient.request(this.enableLoggingRequestOpts); return true; } catch (error) { if (error instanceof TwilioCliError && error.exitCode === 20404) { this.emit('error', new TwilioCliError('Unknown device sid', error.exitCode, error.data)); } else { this.emit('error', error); } } } async onOpen() { this.connectionEstablished = true; this.reconnectRetryCount = 0; this.ws.on('pong', () => this.onPong()); this.ws.on('close', (code, reason) => this.onClose(code, reason)); this.ws.on('message', jsonLog => this.emit('log', jsonLog)); this.emit('open'); // enable logging on the device and set an interval to refresh this periodically await this.enableDeviceLogging(); this.refreshDeviceLoggingHandle = setInterval(() => this.onRefreshDeviceLoggingInterval(), DEVICE_LOGGING_REFRESH_INTERVAL_MS); // enable periodic websocket pings this.pingIntervalHandle = setInterval(() => this.onPingInterval(), PING_INTERVAL_MS); } onClose(code, _reason) { this.cleanup(); if (code !== 4000) { // Device server closes with code 4000 to indicate another stream has replaced this one // For any other close reason, we should attempt to reconnect this.reconnectWithBackoff(); } } async onRefreshDeviceLoggingInterval() { await this.enableDeviceLogging(); } onPingInterval() { this.ws.ping(); this.pingTimeout = setTimeout(() => this.onPingTimeout(), PING_TIMEOUT_MS); } onPong() { if (this.pingTimeout) { clearTimeout(this.pingTimeout); this.pingTimeout = null; } } onPingTimeout() { this.onError(new Error('Ping timeout')); // This will trigger the onClose, which will trigger reconnectWithBackoff this.ws.terminate(); } onUnexpectedResponse(_req, _res) { // remove the error listener, because it'll trigger when we close the websocket this.ws.removeAllListeners('error'); // and add a no-op listener to prevent the unhandled event from throwing an exception this.ws.addListener('error', () => { }); // now close the websocket this.ws.close(); } onError(err) { if (this.connectionEstablished) { this.cleanup(); this.emit('error', err); } else { // connection establishment failed this.emit('error', new Error('Failed to connect')); } this.reconnectWithBackoff(); } write(data) { this.ws.send(data); } close() { // this is a deliberate close so disable the onClose listener // so we don't trigger the automatic reconnect this.ws.removeAllListeners('close'); if (this.connectionEstablished) { this.cleanup(); if (this.ws) { this.ws.close(); } } } cleanup() { if (this.refreshDeviceLoggingHandle) { clearInterval(this.refreshDeviceLoggingHandle); delete this.refreshDeviceLoggingHandle; } if (this.pingIntervalHandle) { clearInterval(this.pingIntervalHandle); delete this.pingIntervalHandle; } if (this.pingTimeout) { clearTimeout(this.pingTimeout); this.pingTimeout = null; } } } module.exports = DeviceLogClient;