diff --git a/package-lock.json b/package-lock.json index 20a17db84..505303fd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3789,9 +3789,9 @@ "dev": true }, "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.5.tgz", + "integrity": "sha512-0Ce31oWVB7YidkaTq33ZxEbN+UDxMMgThvCe8ptgQViymL5DPis9uLdTA13MiRPhgvqyxIegugrP97iK3JeBHg==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -3920,9 +3920,9 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, "https-proxy-agent": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.2.tgz", - "integrity": "sha512-c8Ndjc9Bkpfx/vCJueCPy0jlP4ccCCSNDp8xwCZzPjKJUm+B+u9WX2x98Qx4n1PiMNTWo3D7KK5ifNV/yJyRzg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.3.tgz", + "integrity": "sha512-Ytgnz23gm2DVftnzqRRz2dOXZbGd2uiajSw/95bPp6v53zPRspQjLm/AfBgqbJ2qfeRXWIOMVLpp86+/5yX39Q==", "dev": true, "requires": { "agent-base": "^4.3.0", @@ -4694,9 +4694,9 @@ } }, "lolex": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", - "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.1.tgz", + "integrity": "sha512-dEwHz1CJ8DsdgfpiimgQQEhEJYOEiJ69a0s4aJDNHajaTqOJuF34vBAWVa/sS0V8aQvt72p+KgQ3pRmEVJM+iA==", "dev": true }, "loose-envify": { @@ -5108,6 +5108,14 @@ "just-extend": "^4.0.2", "lolex": "^4.1.0", "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "lolex": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", + "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==", + "dev": true + } } }, "no-case": { @@ -6613,6 +6621,12 @@ "supports-color": "^5.5.0" }, "dependencies": { + "lolex": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", + "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==", + "dev": true + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -7339,20 +7353,20 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.4.tgz", + "integrity": "sha512-9Yc2i881pF4BPGhjteCXQNaXx1DCwm3dtOyBaG2hitHjLWOczw/ki8vD1bqyT3u6K0Ms/FpCShkmfg+FtlOfYA==", "dev": true, "optional": true, "requires": { - "commander": "~2.20.0", + "commander": "~2.20.3", "source-map": "~0.6.1" }, "dependencies": { "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "optional": true }, diff --git a/package.json b/package.json index c3a24f995..287c795ac 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "eslint": "^5.16.0", "eslint-friendly-formatter": "^4.0.1", "eslint-loader": "^2.2.1", + "lolex": "^5.1.1", "mocha": "6.2.0", "mock-require": "^3.0.3", "nyc": "^14.1.1", diff --git a/src/protocols/abstract/realtime.js b/src/protocols/abstract/realtime.js index 30d9203de..47852a01d 100644 --- a/src/protocols/abstract/realtime.js +++ b/src/protocols/abstract/realtime.js @@ -4,7 +4,6 @@ const KuzzleAbstractProtocol = require('./common'); class RTWrapper extends KuzzleAbstractProtocol { - constructor (host, options = {}) { super(host, options); @@ -52,7 +51,7 @@ class RTWrapper extends KuzzleAbstractProtocol { * * @param {Error} error */ - clientNetworkError(error) { + clientNetworkError (error) { this.state = 'offline'; this.clear(); @@ -60,19 +59,35 @@ class RTWrapper extends KuzzleAbstractProtocol { connectionError.internal = error; this.emit('networkError', connectionError); + if (this.autoReconnect && !this.retrying && !this.stopRetryingToConnect) { this.retrying = true; + if ( typeof window === 'object' + && typeof window.navigator === 'object' + && window.navigator.onLine === false + ) { + window.addEventListener( + 'online', + () => { + this.retrying = false; + this.connect().catch(err => this.clientNetworkError(err)); + }, + { once: true }); + return; + } + setTimeout(() => { this.retrying = false; this.connect(this.host).catch(err => this.clientNetworkError(err)); }, this.reconnectionDelay); - } else { + } + else { this.emit('disconnect'); } } - isReady() { + isReady () { return this.state === 'connected'; } } diff --git a/test/mocks/window.mock.js b/test/mocks/window.mock.js new file mode 100644 index 000000000..b1bd8f5ea --- /dev/null +++ b/test/mocks/window.mock.js @@ -0,0 +1,36 @@ +const + sinon = require('sinon'), + KuzzleEventEmitter = require('../../src/eventEmitter'); + +// A class to mock the global window object +class WindowMock extends KuzzleEventEmitter { + constructor () { + super(); + + if (typeof window !== 'undefined') { + throw new Error('Cannot mock add a global "window" object: already defined'); + } + + this.navigator = { + onLine: true + }; + + this.addEventListener = this.addListener; + sinon.spy(this, 'addEventListener'); + } + + static restore () { + delete global.window; + } + + static inject () { + Object.defineProperty(global, 'window', { + value: new this(), + enumerable: false, + writable: false, + configurable: true + }); + } +} + +module.exports = WindowMock; diff --git a/test/protocol/socketio.test.js b/test/protocol/socketio.test.js index 33dceb257..5ea75c8ed 100644 --- a/test/protocol/socketio.test.js +++ b/test/protocol/socketio.test.js @@ -1,11 +1,8 @@ const should = require('should'), sinon = require('sinon'), - SocketIO = require('../../src/protocols/socketio'); - -/** - * @global window - */ + SocketIO = require('../../src/protocols/socketio'), + windowMock = require('../mocks/window.mock'); describe('SocketIO networking module', () => { let @@ -15,6 +12,7 @@ describe('SocketIO networking module', () => { beforeEach(() => { clock = sinon.useFakeTimers(); + socketStub = { events: {}, eventOnce: {}, @@ -74,11 +72,13 @@ describe('SocketIO networking module', () => { }); socketIO.socket = socketStub; - window = {io: sinon.stub().returns(socketStub)}; // eslint-disable-line + windowMock.inject(); + window.io = sinon.stub().returns(socketStub); }); afterEach(() => { clock.restore(); + windowMock.restore(); }); it('should expose an unique identifier', () => { @@ -225,8 +225,6 @@ describe('SocketIO exposed methods', () => { socketIO = new SocketIO('address'); socketIO.socket = socketStub; - - window = {io: sinon.stub().returns(socketStub)}; // eslint-disable-line }); it('should be able to listen to an event just once', () => { diff --git a/test/protocol/websocket.test.js b/test/protocol/websocket.test.js index 84f5ac9f6..0bf8adb02 100644 --- a/test/protocol/websocket.test.js +++ b/test/protocol/websocket.test.js @@ -1,8 +1,10 @@ const should = require('should'), sinon = require('sinon'), + lolex = require('lolex'), NodeWS = require('ws'), - WS = require('../../src/protocols/websocket'); + WS = require('../../src/protocols/websocket'), + windowMock = require('../mocks/window.mock'); describe('WebSocket networking module', () => { let @@ -12,13 +14,13 @@ describe('WebSocket networking module', () => { clientStub; beforeEach(() => { - clock = sinon.useFakeTimers(); + clock = lolex.install(); clientStub = { send: sinon.stub(), close: sinon.stub() }; - window = 'foobar'; // eslint-disable-line + windowMock.inject(); WebSocket = function (...args) { // eslint-disable-line wsargs = args; return clientStub; @@ -32,9 +34,9 @@ describe('WebSocket networking module', () => { }); afterEach(() => { - clock.restore(); + clock.uninstall(); WebSocket = undefined; // eslint-disable-line - window = undefined; // eslint-disable-line + windowMock.restore(); }); it('should expose an unique identifier', () => { @@ -156,6 +158,59 @@ describe('WebSocket networking module', () => { return should(promise).be.rejectedWith('foobar'); }); + it('should stop reconnecting if the browser goes offline', () => { + const cb = sinon.stub(); + + websocket.retrying = false; + websocket.addListener('networkError', cb); + should(websocket.listeners('networkError').length).be.eql(1); + + websocket.connect(); + websocket.connect = sinon.stub().rejects(); + clientStub.onopen(); + clientStub.onerror(); + + should(websocket.retrying).be.true(); + should(cb).be.calledOnce(); + should(websocket.connect).not.be.called(); + + window.navigator.onLine = false; + + return clock.tickAsync(100) + .then(() => { + should(websocket.retrying).be.true(); + should(cb).be.calledTwice(); + should(websocket.connect).be.calledOnce(); + + should(window.addEventListener).calledWith( + 'online', + sinon.match.func, + { once: true }); + + return clock.tickAsync(100); + }) + .then(() => { + // the important bit is there: cb hasn't been called since the last + // tick because the SDK does not try to connect if the browser is + // marked offline + should(cb).be.calledTwice(); + + should(websocket.retrying).be.true(); + should(websocket.connect).be.calledOnce(); + + window.emit('online'); + return clock.tickAsync(100); + }) + .then(() => { + // And it started retrying to connect again now that the browser is + // "online" + should(cb).be.calledThrice(); + + should(websocket.retrying).be.true(); + should(websocket.connect).be.calledTwice(); + }); + }); + it('should call listeners on a "disconnect" event', () => { const cb = sinon.stub(); @@ -371,7 +426,7 @@ describe('WebSocket networking module', () => { it('should fallback to the ws module if there is no global WebSocket API', () => { WebSocket = undefined; // eslint-disable-line - window = undefined; // eslint-disable-line + windowMock.restore(); const client = new WS('foobar'); @@ -391,7 +446,7 @@ describe('WebSocket networking module', () => { it('should initialize pass allowed options to the ws ctor when using it', () => { WebSocket = undefined; // eslint-disable-line - window = undefined; // eslint-disable-line + windowMock.restore(); let client = new WS('foobar'); @@ -413,7 +468,7 @@ describe('WebSocket networking module', () => { it('should throw if invalid options are provided', () => { WebSocket = undefined; // eslint-disable-line - window = undefined; // eslint-disable-line + windowMock.restore(); const invalidHeaders = ['foo', 'false', 'true', 123, []];