From ca48afadb90c9acb5e5b6ac7ae6039937a2a39b3 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Fri, 16 Nov 2018 15:29:38 -0500 Subject: [PATCH 1/7] Add exponential backoff for cloud provider. Uses randomized duration between [0, 2^k-1] where k is the connection attempt number, maxed out at 5. I had to rework the tests quite a bit to make this easy to test, to work around the timeouts and the randomization. --- src/lib/cloud-provider.js | 44 ++++++++--- test/unit/util/cloud-provider.test.js | 102 ++++++++++++++++++++++---- 2 files changed, 120 insertions(+), 26 deletions(-) diff --git a/src/lib/cloud-provider.js b/src/lib/cloud-provider.js index 8116813eb..1e6fb2ade 100644 --- a/src/lib/cloud-provider.js +++ b/src/lib/cloud-provider.js @@ -16,24 +16,26 @@ class CloudProvider { this.vm = vm; this.username = username; this.projectId = projectId; + this.cloudHost = cloudHost; - // Open a websocket connection to the clouddata server - this.openConnection(cloudHost); + this.connectionAttempts = 0; + + this.openConnection(); } /** * Open a new websocket connection to the clouddata server. * @param {string} cloudHost The cloud data server to connect to. */ - openConnection (cloudHost) { - if (window.WebSocket === null) { - log.warn('Websocket support is not available in this browser'); + openConnection () { + try { + this.connection = new WebSocket((location.protocol === 'http:' ? 'ws://' : 'wss://') + this.cloudHost); + } catch (e) { + log.warn('Websocket support is not available in this browser', e); this.connection = null; return; } - this.connection = new WebSocket((location.protocol === 'http:' ? 'ws://' : 'wss://') + cloudHost); - this.connection.onerror = this.onError.bind(this); this.connection.onmessage = this.onMessage.bind(this); this.connection.onopen = this.onOpen.bind(this); @@ -42,8 +44,7 @@ class CloudProvider { onError (event) { log.error(`Websocket connection error: ${JSON.stringify(event)}`); - // TODO Add re-connection attempt logic here - this.clear(); + // Error is always followed by close, which handles reconnect logic. } onMessage (event) { @@ -57,12 +58,31 @@ class CloudProvider { } onOpen () { + this.connectionAttempts = 1; // Reset because we successfully connected this.writeToServer('handshake'); log.info(`Successfully connected to clouddata server.`); } onClose () { log.info(`Closed connection to websocket`); + const exponentialTimeout = (Math.pow(2, this.connectionAttempts) - 1) * 1000; + const randomizedTimeout = this.randomizeDuration(exponentialTimeout); + this.setTimeout(this.reconnectNow.bind(this), randomizedTimeout); + } + + reconnectNow () { + // Max connection attempts at 5, so timeout will max out in range [0, 31s] + this.connectionAttempts = Math.min(this.connectionAttempts + 1, 5); + this.openConnection(); + } + + randomizeDuration (t) { + return Math.random() * t; + } + + setTimeout (fn, time) { + log.info(`Reconnecting in ${time}ms, attempt ${this.connectionAttempts}`); + this._connectionTimeout = window.setTimeout(fn, time); } parseMessage (message) { @@ -148,7 +168,7 @@ class CloudProvider { if (this.connection && this.connection.readyState !== WebSocket.CLOSING && this.connection.readyState !== WebSocket.CLOSED) { - + this.connection.onclose = () => {}; // Remove close listener to prevent reconnect this.connection.close(); } this.clear(); @@ -163,6 +183,10 @@ class CloudProvider { this.vm = null; this.username = null; this.projectId = null; + if (this._connectionTimeout) { + clearTimeout(this._connectionTimeout); + this._connectionTimeout = null; + } } } diff --git a/test/unit/util/cloud-provider.test.js b/test/unit/util/cloud-provider.test.js index c81588dde..d5f8a7112 100644 --- a/test/unit/util/cloud-provider.test.js +++ b/test/unit/util/cloud-provider.test.js @@ -1,33 +1,51 @@ import CloudProvider from '../../../src/lib/cloud-provider'; -// Disable window.WebSocket -global.WebSocket = null; +let websocketConstructorCount = 0; + +// Stub the global websocket so we can call open/close/error/send on it +global.WebSocket = function (url) { + this._url = url; + this._sentMessages = []; + + // These are not real websocket methods, but used to trigger callbacks + this._open = () => this.onopen(); + this._error = e => this.onerror(e); + this._receive = msg => this.onmessage(msg); + + // Stub the real websocket.send to store sent messages + this.send = msg => this._sentMessages.push(msg); + this.close = () => this.onclose(); + + websocketConstructorCount++; +}; +global.WebSocket.CLOSING = 'CLOSING'; +global.WebSocket.CLOSED = 'CLOSED'; describe('CloudProvider', () => { let cloudProvider = null; - let sentMessage = null; let vmIOData = []; - + let timeout = 0; beforeEach(() => { vmIOData = []; cloudProvider = new CloudProvider(); - // Stub connection - cloudProvider.connection = { - send: msg => { - sentMessage = msg; - } - }; // Stub vm cloudProvider.vm = { postIOData: (_namespace, data) => { vmIOData.push(data); } }; + // Stub setTimeout so this can run instantly. + cloudProvider.setTimeout = (fn, after) => { + timeout = after; + fn(); + }; + // Stub randomize to make it consistent for testing. + cloudProvider.randomizeDuration = t => t; }); test('updateVariable', () => { cloudProvider.updateVariable('hello', 1); - const obj = JSON.parse(sentMessage); + const obj = JSON.parse(cloudProvider.connection._sentMessages[0]); expect(obj.method).toEqual('set'); expect(obj.name).toEqual('hello'); expect(obj.value).toEqual(1); @@ -35,7 +53,7 @@ describe('CloudProvider', () => { test('updateVariable with falsey value', () => { cloudProvider.updateVariable('hello', 0); - const obj = JSON.parse(sentMessage); + const obj = JSON.parse(cloudProvider.connection._sentMessages[0]); expect(obj.method).toEqual('set'); expect(obj.name).toEqual('hello'); expect(obj.value).toEqual(0); @@ -43,7 +61,7 @@ describe('CloudProvider', () => { test('writeToServer with falsey index value', () => { cloudProvider.writeToServer('method', 'name', 5, 0); - const obj = JSON.parse(sentMessage); + const obj = JSON.parse(cloudProvider.connection._sentMessages[0]); expect(obj.method).toEqual('method'); expect(obj.name).toEqual('name'); expect(obj.value).toEqual(5); @@ -55,7 +73,7 @@ describe('CloudProvider', () => { method: 'ack', name: 'name' }); - cloudProvider.onMessage({data: msg}); + cloudProvider.connection._receive({data: msg}); expect(vmIOData[0].varCreate.name).toEqual('name'); }); @@ -65,7 +83,7 @@ describe('CloudProvider', () => { name: 'name', value: 'value' }); - cloudProvider.onMessage({data: msg}); + cloudProvider.connection._receive({data: msg}); expect(vmIOData[0].varUpdate.name).toEqual('name'); expect(vmIOData[0].varUpdate.value).toEqual('value'); }); @@ -80,8 +98,60 @@ describe('CloudProvider', () => { method: 'ack', name: 'name2' }); - cloudProvider.onMessage({data: `${msg1}\n${msg2}`}); + cloudProvider.connection._receive({data: `${msg1}\n${msg2}`}); expect(vmIOData[0].varUpdate.name).toEqual('name1'); expect(vmIOData[1].varCreate.name).toEqual('name2'); }); + + test('connecting sets connnection attempts back to 1', () => { + expect(cloudProvider.connectionAttempts).toBe(0); + cloudProvider.connectionAttempts = 10; + cloudProvider.connection._open(); + expect(cloudProvider.connectionAttempts).toBe(1); + }); + + test('disconnect waits for a period equal to 2^k-1 before trying again', () => { + websocketConstructorCount = 1; // This is global, so set it back to 1 to start + + // Connection attempts should still be 0 because connection hasn't opened yet + expect(cloudProvider.connectionAttempts).toBe(0); + cloudProvider.connection._open(); + expect(cloudProvider.connectionAttempts).toBe(1); + + cloudProvider.connection.close(); + expect(timeout).toEqual(1 * 1000); // 2^1 - 1 + expect(websocketConstructorCount).toBe(2); + expect(cloudProvider.connectionAttempts).toBe(2); + + cloudProvider.connection.close(); + expect(timeout).toEqual(3 * 1000); // 2^2 - 1 + expect(websocketConstructorCount).toBe(3); + expect(cloudProvider.connectionAttempts).toBe(3); + + cloudProvider.connection.close(); + expect(timeout).toEqual(7 * 1000); // 2^3 - 1 + expect(websocketConstructorCount).toBe(4); + expect(cloudProvider.connectionAttempts).toBe(4); + + cloudProvider.connection.close(); + expect(timeout).toEqual(15 * 1000); // 2^4 - 1 + expect(websocketConstructorCount).toBe(5); + expect(cloudProvider.connectionAttempts).toBe(5); + + cloudProvider.connection.close(); + expect(timeout).toEqual(31 * 1000); // 2^5 - 1 + expect(websocketConstructorCount).toBe(6); + expect(cloudProvider.connectionAttempts).toBe(5); // Maxed at 5 + + cloudProvider.connection.close(); + expect(timeout).toEqual(31 * 1000); // maxed out at 2^5 - 1 + expect(websocketConstructorCount).toBe(7); + expect(cloudProvider.connectionAttempts).toBe(5); // Maxed at 5 + }); + + test('requestCloseConnection does not try to reconnect', () => { + websocketConstructorCount = 1; // This is global, so set it back to 1 to start + cloudProvider.requestCloseConnection(); + expect(websocketConstructorCount).toBe(1); // No reconnection attempts + }); }); From 388a01e5645c6cf525518de4bf712b97a3196d75 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Fri, 16 Nov 2018 16:13:43 -0500 Subject: [PATCH 2/7] Update src/lib/cloud-provider.js --- src/lib/cloud-provider.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/cloud-provider.js b/src/lib/cloud-provider.js index 1e6fb2ade..5da8a6cc5 100644 --- a/src/lib/cloud-provider.js +++ b/src/lib/cloud-provider.js @@ -187,6 +187,7 @@ class CloudProvider { clearTimeout(this._connectionTimeout); this._connectionTimeout = null; } + this.connectionAttempts = 0; } } From 10b611ad8fa4cc7d9e96586fa7019b06f4c5d2b9 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 19 Nov 2018 10:17:45 -0500 Subject: [PATCH 3/7] Keep track of and log the real connection attempt number and update test --- src/lib/cloud-provider.js | 11 +++++++---- test/unit/util/cloud-provider.test.js | 9 +++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/lib/cloud-provider.js b/src/lib/cloud-provider.js index 1e6fb2ade..421dd0116 100644 --- a/src/lib/cloud-provider.js +++ b/src/lib/cloud-provider.js @@ -65,17 +65,19 @@ class CloudProvider { onClose () { log.info(`Closed connection to websocket`); - const exponentialTimeout = (Math.pow(2, this.connectionAttempts) - 1) * 1000; - const randomizedTimeout = this.randomizeDuration(exponentialTimeout); + const randomizedTimeout = this.randomizeDuration(this.exponentialTimeout()); this.setTimeout(this.reconnectNow.bind(this), randomizedTimeout); } reconnectNow () { - // Max connection attempts at 5, so timeout will max out in range [0, 31s] - this.connectionAttempts = Math.min(this.connectionAttempts + 1, 5); + this.connectionAttempts = this.connectionAttempts + 1; this.openConnection(); } + exponentialTimeout () { + return (Math.pow(2, Math.min(this.connectionAttempts, 5)) - 1) * 1000; + } + randomizeDuration (t) { return Math.random() * t; } @@ -168,6 +170,7 @@ class CloudProvider { if (this.connection && this.connection.readyState !== WebSocket.CLOSING && this.connection.readyState !== WebSocket.CLOSED) { + log.info('Request close cloud connection without reconnecting'); this.connection.onclose = () => {}; // Remove close listener to prevent reconnect this.connection.close(); } diff --git a/test/unit/util/cloud-provider.test.js b/test/unit/util/cloud-provider.test.js index d5f8a7112..3ef4179d2 100644 --- a/test/unit/util/cloud-provider.test.js +++ b/test/unit/util/cloud-provider.test.js @@ -141,12 +141,17 @@ describe('CloudProvider', () => { cloudProvider.connection.close(); expect(timeout).toEqual(31 * 1000); // 2^5 - 1 expect(websocketConstructorCount).toBe(6); - expect(cloudProvider.connectionAttempts).toBe(5); // Maxed at 5 + expect(cloudProvider.connectionAttempts).toBe(6); cloudProvider.connection.close(); expect(timeout).toEqual(31 * 1000); // maxed out at 2^5 - 1 expect(websocketConstructorCount).toBe(7); - expect(cloudProvider.connectionAttempts).toBe(5); // Maxed at 5 + expect(cloudProvider.connectionAttempts).toBe(7); + }); + + test('exponentialTimeout caps connection attempt number', () => { + cloudProvider.connectionAttempts = 1000; + expect(cloudProvider.exponentialTimeout()).toEqual(31 * 1000); }); test('requestCloseConnection does not try to reconnect', () => { From 2b92ff4e06b5bfac8ef69453cfeb3b6cecb5de08 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 19 Nov 2018 11:57:27 -0500 Subject: [PATCH 4/7] Be more careful about when connection attempt is incremented --- src/lib/cloud-provider.js | 10 +++------ src/playground/render-gui.jsx | 5 +++++ test/unit/util/cloud-provider.test.js | 29 +++++++++++---------------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/lib/cloud-provider.js b/src/lib/cloud-provider.js index d1598dc8e..22fdb1cdb 100644 --- a/src/lib/cloud-provider.js +++ b/src/lib/cloud-provider.js @@ -58,20 +58,16 @@ class CloudProvider { } onOpen () { - this.connectionAttempts = 1; // Reset because we successfully connected + this.connectionAttempts = 0; // Reset because we successfully connected this.writeToServer('handshake'); log.info(`Successfully connected to clouddata server.`); } onClose () { log.info(`Closed connection to websocket`); + this.connectionAttempts += 1; const randomizedTimeout = this.randomizeDuration(this.exponentialTimeout()); - this.setTimeout(this.reconnectNow.bind(this), randomizedTimeout); - } - - reconnectNow () { - this.connectionAttempts = this.connectionAttempts + 1; - this.openConnection(); + this.setTimeout(this.openConnection.bind(this), randomizedTimeout); } exponentialTimeout () { diff --git a/src/playground/render-gui.jsx b/src/playground/render-gui.jsx index fac1137e9..84f451f6d 100644 --- a/src/playground/render-gui.jsx +++ b/src/playground/render-gui.jsx @@ -31,6 +31,11 @@ export default appTarget => { if (process.env.NODE_ENV === 'production' && typeof window === 'object') { // Warn before navigating away window.onbeforeunload = () => true; + } else { + window.onerror = e => { + alert('Uncaught error occurred–check the console'); // eslint-disable-line + throw e; + }; } ReactDOM.render( diff --git a/test/unit/util/cloud-provider.test.js b/test/unit/util/cloud-provider.test.js index 3ef4179d2..b803b76d5 100644 --- a/test/unit/util/cloud-provider.test.js +++ b/test/unit/util/cloud-provider.test.js @@ -103,50 +103,45 @@ describe('CloudProvider', () => { expect(vmIOData[1].varCreate.name).toEqual('name2'); }); - test('connecting sets connnection attempts back to 1', () => { + test('connecting sets connnection attempts back to 0', () => { expect(cloudProvider.connectionAttempts).toBe(0); cloudProvider.connectionAttempts = 10; cloudProvider.connection._open(); - expect(cloudProvider.connectionAttempts).toBe(1); + expect(cloudProvider.connectionAttempts).toBe(0); }); test('disconnect waits for a period equal to 2^k-1 before trying again', () => { - websocketConstructorCount = 1; // This is global, so set it back to 1 to start - - // Connection attempts should still be 0 because connection hasn't opened yet - expect(cloudProvider.connectionAttempts).toBe(0); - cloudProvider.connection._open(); - expect(cloudProvider.connectionAttempts).toBe(1); + websocketConstructorCount = 0; // This is global, so set it back to 0 to start cloudProvider.connection.close(); expect(timeout).toEqual(1 * 1000); // 2^1 - 1 + expect(websocketConstructorCount).toBe(1); + expect(cloudProvider.connectionAttempts).toBe(1); + + cloudProvider.connection.close(); + expect(timeout).toEqual(3 * 1000); // 2^2 - 1 expect(websocketConstructorCount).toBe(2); expect(cloudProvider.connectionAttempts).toBe(2); cloudProvider.connection.close(); - expect(timeout).toEqual(3 * 1000); // 2^2 - 1 + expect(timeout).toEqual(7 * 1000); // 2^3 - 1 expect(websocketConstructorCount).toBe(3); expect(cloudProvider.connectionAttempts).toBe(3); cloudProvider.connection.close(); - expect(timeout).toEqual(7 * 1000); // 2^3 - 1 + expect(timeout).toEqual(15 * 1000); // 2^4 - 1 expect(websocketConstructorCount).toBe(4); expect(cloudProvider.connectionAttempts).toBe(4); cloudProvider.connection.close(); - expect(timeout).toEqual(15 * 1000); // 2^4 - 1 + expect(timeout).toEqual(31 * 1000); // 2^5 - 1 expect(websocketConstructorCount).toBe(5); expect(cloudProvider.connectionAttempts).toBe(5); cloudProvider.connection.close(); - expect(timeout).toEqual(31 * 1000); // 2^5 - 1 + expect(timeout).toEqual(31 * 1000); // maxed out at 2^5 - 1 expect(websocketConstructorCount).toBe(6); expect(cloudProvider.connectionAttempts).toBe(6); - - cloudProvider.connection.close(); - expect(timeout).toEqual(31 * 1000); // maxed out at 2^5 - 1 - expect(websocketConstructorCount).toBe(7); - expect(cloudProvider.connectionAttempts).toBe(7); }); test('exponentialTimeout caps connection attempt number', () => { From e651f96bcdee31514cd92f385287f749be08e219 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 19 Nov 2018 11:58:17 -0500 Subject: [PATCH 5/7] Improve logging --- src/lib/cloud-provider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/cloud-provider.js b/src/lib/cloud-provider.js index 22fdb1cdb..c2b545f6e 100644 --- a/src/lib/cloud-provider.js +++ b/src/lib/cloud-provider.js @@ -79,7 +79,7 @@ class CloudProvider { } setTimeout (fn, time) { - log.info(`Reconnecting in ${time}ms, attempt ${this.connectionAttempts}`); + log.info(`Reconnecting in ${Math.floor(time / 1000)}s, attempt ${this.connectionAttempts}`); this._connectionTimeout = window.setTimeout(fn, time); } From a6e4f1f436b81ed2473a449cd233875d1bf251a9 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 19 Nov 2018 12:29:47 -0500 Subject: [PATCH 6/7] Make sure to always wait at least 1 interval before reconnecting. --- src/lib/cloud-provider.js | 13 +++++--- test/unit/util/cloud-provider.test.js | 43 ++++++++++++++++++--------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/lib/cloud-provider.js b/src/lib/cloud-provider.js index c2b545f6e..2e159054c 100644 --- a/src/lib/cloud-provider.js +++ b/src/lib/cloud-provider.js @@ -28,6 +28,8 @@ class CloudProvider { * @param {string} cloudHost The cloud data server to connect to. */ openConnection () { + this.connectionAttempts += 1; + try { this.connection = new WebSocket((location.protocol === 'http:' ? 'ws://' : 'wss://') + this.cloudHost); } catch (e) { @@ -58,14 +60,15 @@ class CloudProvider { } onOpen () { - this.connectionAttempts = 0; // Reset because we successfully connected + // Reset connection attempts to 1 to make sure any subsequent reconnects + // use connectionAttempts=1 to calculate timeout + this.connectionAttempts = 1; this.writeToServer('handshake'); log.info(`Successfully connected to clouddata server.`); } onClose () { log.info(`Closed connection to websocket`); - this.connectionAttempts += 1; const randomizedTimeout = this.randomizeDuration(this.exponentialTimeout()); this.setTimeout(this.openConnection.bind(this), randomizedTimeout); } @@ -79,7 +82,7 @@ class CloudProvider { } setTimeout (fn, time) { - log.info(`Reconnecting in ${Math.floor(time / 1000)}s, attempt ${this.connectionAttempts}`); + log.info(`Reconnecting in ${(time / 1000).toFixed(1)}s, attempt ${this.connectionAttempts}`); this._connectionTimeout = window.setTimeout(fn, time); } @@ -167,7 +170,9 @@ class CloudProvider { this.connection.readyState !== WebSocket.CLOSING && this.connection.readyState !== WebSocket.CLOSED) { log.info('Request close cloud connection without reconnecting'); - this.connection.onclose = () => {}; // Remove close listener to prevent reconnect + // Remove listeners, after this point we do not want to react to connection updates + this.connection.onclose = () => {}; + this.connection.onerror = () => {}; this.connection.close(); } this.clear(); diff --git a/test/unit/util/cloud-provider.test.js b/test/unit/util/cloud-provider.test.js index b803b76d5..ee720044d 100644 --- a/test/unit/util/cloud-provider.test.js +++ b/test/unit/util/cloud-provider.test.js @@ -103,45 +103,60 @@ describe('CloudProvider', () => { expect(vmIOData[1].varCreate.name).toEqual('name2'); }); - test('connecting sets connnection attempts back to 0', () => { - expect(cloudProvider.connectionAttempts).toBe(0); - cloudProvider.connectionAttempts = 10; + test('connnection attempts set back to 1 when socket is opened', () => { + cloudProvider.connectionAttempts = 100; cloudProvider.connection._open(); - expect(cloudProvider.connectionAttempts).toBe(0); + expect(cloudProvider.connectionAttempts).toBe(1); }); test('disconnect waits for a period equal to 2^k-1 before trying again', () => { - websocketConstructorCount = 0; // This is global, so set it back to 0 to start - - cloudProvider.connection.close(); - expect(timeout).toEqual(1 * 1000); // 2^1 - 1 - expect(websocketConstructorCount).toBe(1); + websocketConstructorCount = 1; // This is global, so set it back to 1 to start + // Constructor attempts to open connection, so attempts is initially 1 expect(cloudProvider.connectionAttempts).toBe(1); + // Make sure a close without a previous OPEN still waits 1s before reconnecting cloudProvider.connection.close(); - expect(timeout).toEqual(3 * 1000); // 2^2 - 1 + expect(timeout).toEqual(1 * 1000); // 2^1 - 1 expect(websocketConstructorCount).toBe(2); expect(cloudProvider.connectionAttempts).toBe(2); cloudProvider.connection.close(); - expect(timeout).toEqual(7 * 1000); // 2^3 - 1 + expect(timeout).toEqual(3 * 1000); // 2^2 - 1 expect(websocketConstructorCount).toBe(3); expect(cloudProvider.connectionAttempts).toBe(3); cloudProvider.connection.close(); - expect(timeout).toEqual(15 * 1000); // 2^4 - 1 + expect(timeout).toEqual(7 * 1000); // 2^3 - 1 expect(websocketConstructorCount).toBe(4); expect(cloudProvider.connectionAttempts).toBe(4); cloudProvider.connection.close(); - expect(timeout).toEqual(31 * 1000); // 2^5 - 1 + expect(timeout).toEqual(15 * 1000); // 2^4 - 1 expect(websocketConstructorCount).toBe(5); expect(cloudProvider.connectionAttempts).toBe(5); cloudProvider.connection.close(); - expect(timeout).toEqual(31 * 1000); // maxed out at 2^5 - 1 + expect(timeout).toEqual(31 * 1000); // 2^5 - 1 expect(websocketConstructorCount).toBe(6); expect(cloudProvider.connectionAttempts).toBe(6); + + cloudProvider.connection.close(); + expect(timeout).toEqual(31 * 1000); // maxed out at 2^5 - 1 + expect(websocketConstructorCount).toBe(7); + expect(cloudProvider.connectionAttempts).toBe(7); + }); + + test('close after connection is opened waits 1s before reconnecting', () => { + // This test is basically to check that opening the connection does not impact + // the time until reconnection for the first reconnect. + // It is easy to introduce a bug that causes reconnection time to be different + // based on whether an initial connection was made. + websocketConstructorCount = 1; + cloudProvider.connection._open(); + cloudProvider.connection.close(); + expect(timeout).toEqual(1 * 1000); // 2^1 - 1 + expect(websocketConstructorCount).toBe(2); + expect(cloudProvider.connectionAttempts).toBe(2); }); test('exponentialTimeout caps connection attempt number', () => { From d696368469919f8bb0c489ca66b091650a5f0978 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 19 Nov 2018 13:48:34 -0500 Subject: [PATCH 7/7] Remove unrelated chunk --- src/playground/render-gui.jsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/playground/render-gui.jsx b/src/playground/render-gui.jsx index 84f451f6d..fac1137e9 100644 --- a/src/playground/render-gui.jsx +++ b/src/playground/render-gui.jsx @@ -31,11 +31,6 @@ export default appTarget => { if (process.env.NODE_ENV === 'production' && typeof window === 'object') { // Warn before navigating away window.onbeforeunload = () => true; - } else { - window.onerror = e => { - alert('Uncaught error occurred–check the console'); // eslint-disable-line - throw e; - }; } ReactDOM.render(