diff --git a/.npmignore b/.npmignore index 9d7f4d43..e68fb4bb 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,11 @@ +.circleci +.deepsource.toml .env +.github .idea .nyc_output +.nycrc +.release.json BUILD.md build.yaml config.example.yaml @@ -8,9 +13,13 @@ config.yaml coverage docs extension -lib +karma.conf.ts +karma.network.conf.ts node_modules nodemon.json +PREFLIGHT.md scripts server.js +templates tests +tslint.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 69eadbba..9ad9c888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,13 @@ 2.6.0 (In Progress) =================== -Changes (TODO) +Changes ------- +- Removed usage of NodeJS modules from the SDK and some dependencies. With this change, the SDK should now work with some of the latest frameworks that use the latest versions of bundlers such as Vite and Webpack. +- Removed unnecessary files from the generated npm package. +- Links to source maps are now included in the generated npm package. + 2.5.0 (May 9, 2023) =================== diff --git a/lib/twilio/backoff.ts b/lib/twilio/backoff.ts new file mode 100644 index 00000000..d5329b70 --- /dev/null +++ b/lib/twilio/backoff.ts @@ -0,0 +1,74 @@ +/** + * @packageDocumentation + * @internalapi + */ +// @ts-nocheck +// NOTE (csantos): This file was taken directly from twilio-video and has been renamed from JS to TS only. +// It needs to be re-written as part of the overall updating of the files to TS. +import { EventEmitter } from 'events'; + +class Backoff extends EventEmitter { + /** + * Construct a {@link Backoff}. + * @param {object} options + * @property {number} min - Initial timeout in milliseconds [100] + * @property {number} max - Max timeout [10000] + * @property {boolean} jitter - Apply jitter [0] + * @property {number} factor - Multiplication factor for Backoff operation [2] + */ + constructor(options) { + super(); + Object.defineProperties(this, { + _attempts: { + value: 0, + writable: true, + }, + _duration: { + enumerable: false, + get() { + let ms = this._min * Math.pow(this._factor, this._attempts); + if (this._jitter) { + const rand = Math.random(); + const deviation = Math.floor(rand * this._jitter * ms); + // tslint:disable-next-line + ms = (Math.floor(rand * 10) & 1) === 0 ? ms - deviation : ms + deviation; + } + // tslint:disable-next-line + return Math.min(ms, this._max) | 0; + }, + }, + _factor: { value: options.factor || 2 }, + _jitter: { value: options.jitter > 0 && options.jitter <= 1 ? options.jitter : 0 }, + _max: { value: options.max || 10000 }, + _min: { value: options.min || 100 }, + _timeoutID: { + value: null, + writable: true, + }, + }); + } + + backoff() { + const duration = this._duration; + if (this._timeoutID) { + clearTimeout(this._timeoutID); + this._timeoutID = null; + } + + this.emit('backoff', this._attempts, duration); + this._timeoutID = setTimeout(() => { + this.emit('ready', this._attempts, duration); + this._attempts++; + }, duration); + } + + reset() { + this._attempts = 0; + if (this._timeoutID) { + clearTimeout(this._timeoutID); + this._timeoutID = null; + } + } +} + +export default Backoff; diff --git a/lib/twilio/call.ts b/lib/twilio/call.ts index 578c757d..f16139ba 100644 --- a/lib/twilio/call.ts +++ b/lib/twilio/call.ts @@ -5,6 +5,7 @@ * @internal */ import { EventEmitter } from 'events'; +import Backoff from './backoff'; import Device from './device'; import DialtonePlayer from './dialtonePlayer'; import { @@ -26,7 +27,6 @@ import StatsMonitor from './statsMonitor'; import { isChrome } from './util'; import { generateVoiceEventSid } from './uuid'; -const Backoff = require('backoff'); const { RELEASE_VERSION } = require('./constants'); // Placeholders until we convert the respective files to TypeScript. @@ -53,9 +53,9 @@ export type ISound = any; const BACKOFF_CONFIG = { factor: 1.1, - initialDelay: 1, - maxDelay: 30000, - randomisationFactor: 0.5, + jitter: 0.5, + max: 30000, + min: 1, }; const DTMF_INTER_TONE_GAP: number = 70; @@ -345,7 +345,7 @@ class Call extends EventEmitter { this.callerInfo = null; } - this._mediaReconnectBackoff = Backoff.exponential(BACKOFF_CONFIG); + this._mediaReconnectBackoff = new Backoff(BACKOFF_CONFIG); this._mediaReconnectBackoff.on('ready', () => this._mediaHandler.iceRestart()); // temporary call sid to be used for outgoing calls @@ -1242,7 +1242,7 @@ class Call extends EventEmitter { if (isEndOfIceCycle) { // We already exceeded max retry time. - if (Date.now() - this._mediaReconnectStartTime > BACKOFF_CONFIG.maxDelay) { + if (Date.now() - this._mediaReconnectStartTime > BACKOFF_CONFIG.max) { this._log.info('Exceeded max ICE retries'); return this._mediaHandler.onerror(MEDIA_DISCONNECT_ERROR); } diff --git a/lib/twilio/wstransport.ts b/lib/twilio/wstransport.ts index aa8b0832..2b6b5fe3 100644 --- a/lib/twilio/wstransport.ts +++ b/lib/twilio/wstransport.ts @@ -6,12 +6,10 @@ import { EventEmitter } from 'events'; import * as WebSocket from 'ws'; +import Backoff from './backoff'; import { SignalingErrors } from './errors'; import Log from './log'; -// tslint:disable-next-line -const Backoff = require('backoff'); - const CONNECT_SUCCESS_TIMEOUT = 10000; const CONNECT_TIMEOUT = 5000; const HEARTBEAT_TIMEOUT = 15000; @@ -521,11 +519,12 @@ export default class WSTransport extends EventEmitter { private _setupBackoffs(): typeof WSTransport.prototype._backoff { const preferredBackoffConfig = { factor: 2.0, - maxDelay: this._options.maxPreferredDelayMs, - randomisationFactor: 0.40, + jitter: 0.40, + max: this._options.maxPreferredDelayMs, + min: 100, }; this._log.info('Initializing preferred transport backoff using config: ', preferredBackoffConfig); - const preferredBackoff = Backoff.exponential(preferredBackoffConfig); + const preferredBackoff = new Backoff(preferredBackoffConfig); preferredBackoff.on('backoff', (attempt: number, delay: number) => { if (this.state === WSTransportState.Closed) { @@ -565,16 +564,16 @@ export default class WSTransport extends EventEmitter { 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 - initialDelay: this._uris && this._uris.length > 1 + min: this._uris && this._uris.length > 1 ? Math.floor(Math.random() * (5000 - 1000 + 1)) + 1000 : 100, - maxDelay: this._options.maxPrimaryDelayMs, - randomisationFactor: 0.40, }; this._log.info('Initializing primary transport backoff using config: ', primaryBackoffConfig); - const primaryBackoff = Backoff.exponential(primaryBackoffConfig); + const primaryBackoff = new Backoff(primaryBackoffConfig); primaryBackoff.on('backoff', (attempt: number, delay: number) => { if (this.state === WSTransportState.Closed) { diff --git a/package-lock.json b/package-lock.json index 9416fd82..94a614eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@twilio/audioplayer": "1.0.6", "@twilio/voice-errors": "1.3.1", "@types/md5": "2.3.2", - "backoff": "2.5.0", + "events": "3.3.0", "loglevel": "1.6.7", "md5": "2.3.0", "rtcpeerconnection-shim": "1.2.8", @@ -2157,17 +2157,6 @@ "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==", "dev": true }, - "node_modules/backoff": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", - "integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==", - "dependencies": { - "precond": "0.2" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2539,6 +2528,15 @@ "pako": "~1.0.5" } }, + "node_modules/browserify/node_modules/events": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", + "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/browserslist": { "version": "3.2.8", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz", @@ -4603,12 +4601,11 @@ "dev": true }, "node_modules/events": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", - "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==", - "dev": true, + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "engines": { - "node": ">=0.4.x" + "node": ">=0.8.x" } }, "node_modules/evp_bytestokey": { @@ -7910,15 +7907,6 @@ "util": "^0.12.0" } }, - "node_modules/karma-typescript/node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/karma-typescript/node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -10662,14 +10650,6 @@ "which": "bin/which" } }, - "node_modules/precond": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", - "integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -16901,14 +16881,6 @@ "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==", "dev": true }, - "backoff": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", - "integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==", - "requires": { - "precond": "0.2" - } - }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -17148,6 +17120,14 @@ "util": "~0.10.1", "vm-browserify": "^1.0.0", "xtend": "^4.0.0" + }, + "dependencies": { + "events": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", + "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==", + "dev": true + } } }, "browserify-aes": { @@ -18940,10 +18920,9 @@ "dev": true }, "events": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", - "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==", - "dev": true + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, "evp_bytestokey": { "version": "1.0.3", @@ -21585,12 +21564,6 @@ "util": "^0.12.0" } }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true - }, "path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -23729,11 +23702,6 @@ } } }, - "precond": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", - "integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==" - }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", diff --git a/package.json b/package.json index 67e12aa9..f56d7f0e 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "@twilio/audioplayer": "1.0.6", "@twilio/voice-errors": "1.3.1", "@types/md5": "2.3.2", - "backoff": "2.5.0", + "events": "3.3.0", "loglevel": "1.6.7", "md5": "2.3.0", "rtcpeerconnection-shim": "1.2.8", diff --git a/tests/index.ts b/tests/index.ts index 486714c9..825360d5 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -85,6 +85,7 @@ require('./sound'); require('./sdp'); require('./unit/asyncQueue'); +require('./unit/backoff'); require('./unit/icecandidate'); require('./unit/call'); require('./unit/device'); diff --git a/tests/unit/backoff.ts b/tests/unit/backoff.ts new file mode 100644 index 00000000..bbc094a9 --- /dev/null +++ b/tests/unit/backoff.ts @@ -0,0 +1,143 @@ +//@ts-nocheck +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import Backoff from '../../lib/twilio/backoff'; + +describe('Backoff', () => { + let clock; + beforeEach(() => { + clock = sinon.useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + describe('Default Options', () => { + let options = {}; + let backoff; + let onBackoff; + let onReady; + + beforeEach(() => { + onBackoff = sinon.stub(); + onReady = sinon.stub(); + backoff = new Backoff(options); + backoff.on('backoff', onBackoff); + backoff.on('ready', onReady); + }); + + it('should raise backoff and ready events on start', () => { + backoff.backoff(); + clock.tick(100); + backoff.reset(); + sinon.assert.calledOnce(onBackoff); + sinon.assert.calledOnce(onReady); + sinon.assert.callOrder(onBackoff, onReady); + }); + + it('should increase the duration exponentially', () => { + backoff.backoff(); + clock.tick(110); + sinon.assert.calledOnce(onReady); + backoff.backoff(); + clock.tick(210); + sinon.assert.calledTwice(onReady); + backoff.backoff(); + clock.tick(410); + sinon.assert.calledThrice(onReady); + }); + + it('should reset the duration', () => { + backoff.backoff(); + clock.tick(110); + sinon.assert.calledOnce(onReady); + backoff.reset(); + clock.tick(210); + clock.tick(410); + sinon.assert.calledOnce(onReady); + }); + + it('should reset the duration once', () => { + backoff.backoff(); + clock.tick(110); + sinon.assert.calledOnce(onReady); + backoff.reset(); + backoff.reset(); + clock.tick(210); + clock.tick(410); + sinon.assert.calledOnce(onReady); + }); + + it('should provide number of attempts and duration in the backoff and ready event', () => { + const attempts = []; + const durations = []; + backoff = new Backoff({ max: 5000 }); + backoff.on('ready', (a, d) => { + attempts.push(a); + durations.push(d); + backoff.backoff(); + }); + backoff.backoff(); + clock.tick(2000); + assert.deepStrictEqual(attempts, [0,1,2,3,4]); + assert.deepStrictEqual(durations, [100,100,200,400,800]); + }); + }); + + describe('Custom Options', () => { + [ + { + tickCount: 1550, + testName: 'with max 400', + options: { max: 400 }, + numberCallbackExpected: 5 + }, + { + tickCount: 1550, + testName: 'with max 400', + options: { max: 400, jitter: 0.1 }, + numberCallbackExpected: 5 + }, + { + tickCount: 10000, + testName: 'with min 600', + options: { min: 600 }, + numberCallbackExpected: 5 + }, + { + tickCount: 10000, + testName: 'with factor 3', + options: { factor: 3 }, + numberCallbackExpected: 5 + }, + { + tickCount: 25000, + testName: 'with min 200, max 20000', + options: { min: 200, max: 20000 }, + numberCallbackExpected: 7 + }, + { + tickCount: 25000, + testName: 'with min 200, max 20000, factor 3', + options: { min: 200, max: 20000, factor: 3 }, + numberCallbackExpected: 6 + }, + ].forEach(testCase => { + it(testCase.testName, () => { + const backoff = new Backoff(testCase.options); + + let callbacks = 0; + backoff.on('ready', () => { + backoff.backoff(); + callbacks++; + }); + + backoff.backoff(); + clock.tick(testCase.tickCount); + + assert.strictEqual(callbacks, testCase.numberCallbackExpected); + }); + }); + }); +});