diff --git a/docs/index.md b/docs/index.md index 2fb9766b..74a4c8b0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -61,6 +61,8 @@ - [API Events](#api-events) - [ledger](#ledger) - [error](#error) + - [connected](#connected) + - [disconnected](#disconnected) @@ -90,6 +92,14 @@ const api = new RippleAPI({ api.on('error', (errorCode, errorMessage) => { console.log(errorCode + ': ' + errorMessage); }); +api.on('connected', () => { + console.log('connected'); +}); +api.on('disconnected', (code) => { + // code - [close code](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent) sent by the server + // will be 1000 if this was normal closure + console.log('disconnected, code:', code); +}); api.connect().then(() => { /* insert code here */ }).then(() => { @@ -3593,3 +3603,35 @@ api.on('error', (errorCode, errorMessage, data) => { tooBusy: The server is too busy to help you now. ``` +## connected + +This event is emitted after connection successfully opened. + +### Example + +```javascript +api.on('connected', () => { + console.log('Connection is open now.'); +}); +``` + +## disconnected + +This event is emitted when connection is closed. + +### Return Value + +The only parameter is a number containing the [close code](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent) send by the server. + +### Example + +```javascript +api.on('disconnected', (code) => { + if (code !== 1000) { + console.log('Connection is closed due to error.'); + } else { + console.log('Connection is closed normally.'); + } +}); +``` + diff --git a/docs/src/boilerplate.md.ejs b/docs/src/boilerplate.md.ejs index 2e9feefe..265e9605 100644 --- a/docs/src/boilerplate.md.ejs +++ b/docs/src/boilerplate.md.ejs @@ -11,6 +11,14 @@ const api = new RippleAPI({ api.on('error', (errorCode, errorMessage) => { console.log(errorCode + ': ' + errorMessage); }); +api.on('connected', () => { + console.log('connected'); +}); +api.on('disconnected', (code) => { + // code - [close code](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent) sent by the server + // will be 1000 if this was normal closure + console.log('disconnected, code:', code); +}); api.connect().then(() => { /* insert code here */ }).then(() => { diff --git a/docs/src/events.md.ejs b/docs/src/events.md.ejs index 11fe3ba3..828a0419 100644 --- a/docs/src/events.md.ejs +++ b/docs/src/events.md.ejs @@ -47,3 +47,35 @@ api.on('error', (errorCode, errorMessage, data) => { ``` tooBusy: The server is too busy to help you now. ``` + +## connected + +This event is emitted after connection successfully opened. + +### Example + +```javascript +api.on('connected', () => { + console.log('Connection is open now.'); +}); +``` + +## disconnected + +This event is emitted when connection is closed. + +### Return Value + +The only parameter is a number containing the [close code](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent) send by the server. + +### Example + +```javascript +api.on('disconnected', (code) => { + if (code !== 1000) { + console.log('Connection is closed due to error.'); + } else { + console.log('Connection is closed normally.'); + } +}); +``` diff --git a/src/api.js b/src/api.js index b8556b6e..9cd635af 100644 --- a/src/api.js +++ b/src/api.js @@ -1,5 +1,5 @@ /* @flow */ -'use strict'; +'use strict'; // eslint-disable-line /* eslint-disable max-len */ // Enable core-js polyfills. This allows use of ES6/7 extensions listed here: @@ -89,6 +89,12 @@ class RippleAPI extends EventEmitter { this.connection.on('error', (errorCode, errorMessage, data) => { this.emit('error', errorCode, errorMessage, data); }); + this.connection.on('connected', () => { + this.emit('connected'); + }); + this.connection.on('disconnected', onError => { + this.emit('disconnected', onError); + }); } else { // use null object pattern to provide better error message if user // tries to call a method that requires a connection diff --git a/src/common/connection.js b/src/common/connection.js index bbf68d94..42c92ab1 100644 --- a/src/common/connection.js +++ b/src/common/connection.js @@ -1,3 +1,5 @@ +'use strict'; // eslint-disable-line + const _ = require('lodash'); const {EventEmitter} = require('events'); const WebSocket = require('ws'); @@ -97,36 +99,42 @@ class Connection extends EventEmitter { return this._state === WebSocket.OPEN && this._isReady; } - _onUnexpectedClose(resolve, reject) { + _onUnexpectedClose(beforeOpen, resolve, reject, code) { if (this._onOpenErrorBound) { this._ws.removeListener('error', this._onOpenErrorBound); this._onOpenErrorBound = null; } this._ws = null; this._isReady = false; - if (_.isFunction(resolve)) { + if (beforeOpen) { // connection was closed before it was properly opened, so we must return // error to connect's caller this.connect().then(resolve, reject); } else { - this.emit('disconnected', true); + // if first parameter ws lib sends close code, + // but sometimes it forgots about it, so default to 1006 - CLOSE_ABNORMAL + this.emit('disconnected', code || 1006); this._retryConnect(); } } - _retryConnect() { - this._retry += 1; - const retryTimeout = (this._retry < 40) + _calculateTimeout(retriesCount) { + return (retriesCount < 40) // First, for 2 seconds: 20 times per second ? (1000 / 20) - : (this._retry < 40 + 60) + : (retriesCount < 40 + 60) // Then, for 1 minute: once per second ? (1000) - : (this._retry < 40 + 60 + 60) + : (retriesCount < 40 + 60 + 60) // Then, for 10 minutes: once every 10 seconds ? (10 * 1000) // Then: once every 30 seconds : (30 * 1000); + } + + _retryConnect() { + this._retry += 1; + const retryTimeout = this._calculateTimeout(this._retry); this._retryTimer = setTimeout(() => { this.connect().catch(this._retryConnect.bind(this)); }, retryTimeout); @@ -139,7 +147,8 @@ class Connection extends EventEmitter { _onOpen() { this._ws.removeListener('close', this._onUnexpectedCloseBound); - this._onUnexpectedCloseBound = this._onUnexpectedClose.bind(this); + this._onUnexpectedCloseBound = + this._onUnexpectedClose.bind(this, false, null, null); this._ws.once('close', this._onUnexpectedCloseBound); this._ws.removeListener('error', this._onOpenErrorBound); @@ -234,7 +243,7 @@ class Connection extends EventEmitter { // resolve connect's promise after reconnect in that case. // after open event we will rebound _onUnexpectedCloseBound // without resolve and reject functions - this._onUnexpectedCloseBound = this._onUnexpectedClose.bind(this, + this._onUnexpectedCloseBound = this._onUnexpectedClose.bind(this, true, resolve, reject); this._ws.once('close', this._onUnexpectedCloseBound); this._ws.once('open', () => this._onOpen().then(resolve, reject)); @@ -252,10 +261,10 @@ class Connection extends EventEmitter { this._ws.once('close', resolve); } else { this._ws.removeListener('close', this._onUnexpectedCloseBound); - this._ws.once('close', () => { + this._ws.once('close', code => { this._ws = null; this._isReady = false; - this.emit('disconnected', false); + this.emit('disconnected', code || 1000); // 1000 - CLOSE_NORMAL resolve(); }); this._ws.close(); diff --git a/test/connection-test.js b/test/connection-test.js index 650aa6e8..1cb5c5f8 100644 --- a/test/connection-test.js +++ b/test/connection-test.js @@ -1,3 +1,4 @@ +'use strict'; // eslint-disable-line /* eslint-disable max-nested-callbacks */ const _ = require('lodash'); @@ -212,7 +213,9 @@ describe('Connection', function() { let connectsCount = 0; let disconnectsCount = 0; - this.api.connection.on('disconnected', () => { + let code = 0; + this.api.connection.on('disconnected', _code => { + code = _code; disconnectsCount += 1; }); this.api.connection.on('connected', () => { @@ -224,6 +227,9 @@ describe('Connection', function() { if (disconnectsCount !== 3) { done(new Error('disconnectsCount must be equal to 3 (got ' + disconnectsCount + ' instead)')); + } else if (code !== 1006) { + done(new Error('disconnect must send code 1006 (got ' + code + + ' instead)')); } else { done(); } @@ -233,6 +239,36 @@ describe('Connection', function() { breakConnection(); }); + it('should emit disconnected event with code 1000 (CLOSE_NORMAL)', + function(done + ) { + this.api.once('disconnected', code => { + assert.strictEqual(code, 1000); + done(); + }); + this.api.disconnect(); + }); + + it('should emit disconnected event with code 1006 (CLOSE_ABNORMAL)', + function(done + ) { + if (process.browser) { + // can't be tested in browser this way, so skipping + done(); + return; + } + this.api.once('disconnected', code => { + assert.strictEqual(code, 1006); + done(); + }); + this.mockRippled.close(); + }); + + it('should emit connected event on after reconnect', function(done) { + this.api.once('connected', done); + this.api.connection._ws.close(); + }); + it('Multiply connect calls', function() { return this.api.connect().then(() => { return this.api.connect(); diff --git a/test/setup-api.js b/test/setup-api.js index 9353927a..5d793cbd 100644 --- a/test/setup-api.js +++ b/test/setup-api.js @@ -1,3 +1,5 @@ +'use strict'; // eslint-disable-line + const net = require('net'); const RippleAPI = require('ripple-api').RippleAPI; const RippleAPIBroadcast = require('ripple-api').RippleAPIBroadcast;