diff --git a/js/src/api/transport/ws/ws.js b/js/src/api/transport/ws/ws.js index 1cb1fb1c4..7b214fded 100644 --- a/js/src/api/transport/ws/ws.js +++ b/js/src/api/transport/ws/ws.js @@ -84,7 +84,7 @@ export default class Ws extends JsonRpcBase { this._connecting = false; if (this._autoConnect) { - this._connect(); + setTimeout(() => this._connect(), 500); } } diff --git a/js/src/redux/actions.js b/js/src/redux/actions.js index 58e9c2a36..bb5f42a33 100644 --- a/js/src/redux/actions.js +++ b/js/src/redux/actions.js @@ -16,7 +16,7 @@ import { newError } from '../ui/Errors/actions'; import { setAddressImage } from './providers/imagesActions'; -import { clearStatusLogs, toggleStatusLogs } from './providers/statusActions'; +import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from './providers/statusActions'; import { toggleView } from '../views/Settings'; export { @@ -24,5 +24,6 @@ export { clearStatusLogs, setAddressImage, toggleStatusLogs, + toggleStatusRefresh, toggleView }; diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index 9f47517f5..4ef8d3de6 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -15,32 +15,29 @@ // along with Parity. If not, see . import { statusBlockNumber, statusCollection, statusLogs } from './statusActions'; +import { isEqual } from 'lodash'; export default class Status { constructor (store, api) { this._api = api; this._store = store; + + this._pingable = false; + this._apiStatus = {}; + this._status = {}; + this._longStatus = {}; + this._minerSettings = {}; + + this._pollPingTimeoutId = null; + this._longStatusTimeoutId = null; } start () { this._subscribeBlockNumber(); this._pollPing(); this._pollStatus(); + this._pollLongStatus(); this._pollLogs(); - this._fetchEnode(); - } - - _fetchEnode () { - this._api.parity - .enode() - .then((enode) => { - this._store.dispatch(statusCollection({ enode })); - }) - .catch(() => { - window.setTimeout(() => { - this._fetchEnode(); - }, 1000); - }); } _subscribeBlockNumber () { @@ -57,10 +54,43 @@ export default class Status { }); } + /** + * Pinging should be smart. It should only + * be used when the UI is connecting or the + * Node is deconnected. + * + * @see src/views/Connection/connection.js + */ + _shouldPing = () => { + const { isConnected, isConnecting } = this._apiStatus; + return isConnecting || !isConnected; + } + + _stopPollPing = () => { + if (!this._pollPingTimeoutId) { + return; + } + + clearTimeout(this._pollPingTimeoutId); + this._pollPingTimeoutId = null; + } + _pollPing = () => { - const dispatch = (status, timeout = 500) => { - this._store.dispatch(statusCollection({ isPingable: status })); - setTimeout(this._pollPing, timeout); + // Already pinging, don't try again + if (this._pollPingTimeoutId) { + return; + } + + const dispatch = (pingable, timeout = 1000) => { + if (pingable !== this._pingable) { + this._pingable = pingable; + this._store.dispatch(statusCollection({ isPingable: pingable })); + } + + this._pollPingTimeoutId = setTimeout(() => { + this._stopPollPing(); + this._pollPing(); + }, timeout); }; fetch('/', { method: 'HEAD' }) @@ -79,61 +109,162 @@ export default class Status { } _pollStatus = () => { - const { secureToken, isConnected, isConnecting, needsToken } = this._api; - const nextTimeout = (timeout = 1000) => { setTimeout(this._pollStatus, timeout); }; - this._store.dispatch(statusCollection({ isConnected, isConnecting, needsToken, secureToken })); + const { isConnected, isConnecting, needsToken, secureToken } = this._api; + + const apiStatus = { + isConnected, + isConnecting, + needsToken, + secureToken + }; + + const gotReconnected = !this._apiStatus.isConnected && apiStatus.isConnected; + + if (gotReconnected) { + this._pollLongStatus(); + } + + if (!isEqual(apiStatus, this._apiStatus)) { + this._store.dispatch(statusCollection(apiStatus)); + this._apiStatus = apiStatus; + } + + // Ping if necessary, otherwise stop pinging + if (this._shouldPing()) { + this._pollPing(); + } else { + this._stopPollPing(); + } if (!isConnected) { - nextTimeout(250); - return; + return nextTimeout(250); } + const { refreshStatus } = this._store.getState().nodeStatus; + + const statusPromises = [ this._api.eth.syncing() ]; + + if (refreshStatus) { + statusPromises.push(this._api.eth.hashrate()); + statusPromises.push(this._api.parity.netPeers()); + } + + Promise + .all(statusPromises) + .then((statusResults) => { + const status = statusResults.length === 1 + ? { + syncing: statusResults[0] + } + : { + syncing: statusResults[0], + hashrate: statusResults[1], + netPeers: statusResults[2] + }; + + if (!isEqual(status, this._status)) { + this._store.dispatch(statusCollection(status)); + this._status = status; + } + + nextTimeout(); + }) + .catch((error) => { + console.error('_pollStatus', error); + nextTimeout(250); + }); + } + + /** + * Miner settings should never changes unless + * Parity is restarted, or if the values are changed + * from the UI + */ + _pollMinerSettings = () => { + Promise + .all([ + this._api.eth.coinbase(), + this._api.parity.extraData(), + this._api.parity.minGasPrice(), + this._api.parity.gasFloorTarget() + ]) + .then(([ + coinbase, extraData, minGasPrice, gasFloorTarget + ]) => { + const minerSettings = { + coinbase, + extraData, + minGasPrice, + gasFloorTarget + }; + + if (!isEqual(minerSettings, this._minerSettings)) { + this._store.dispatch(statusCollection(minerSettings)); + this._minerSettings = minerSettings; + } + }) + .catch((error) => { + console.error('_pollMinerSettings', error); + }); + } + + /** + * The data fetched here should not change + * unless Parity is restarted. They are thus + * fetched every 30s just in case, and whenever + * the client got reconnected. + */ + _pollLongStatus = () => { + const nextTimeout = (timeout = 30000) => { + if (this._longStatusTimeoutId) { + clearTimeout(this._longStatusTimeoutId); + } + + this._longStatusTimeoutId = setTimeout(this._pollLongStatus, timeout); + }; + + // Poll Miner settings just in case + this._pollMinerSettings(); + Promise .all([ this._api.web3.clientVersion(), - this._api.eth.coinbase(), this._api.parity.defaultExtraData(), - this._api.parity.extraData(), - this._api.parity.gasFloorTarget(), - this._api.eth.hashrate(), - this._api.parity.minGasPrice(), this._api.parity.netChain(), - this._api.parity.netPeers(), this._api.parity.netPort(), - this._api.parity.nodeName(), this._api.parity.rpcSettings(), - this._api.eth.syncing() + this._api.parity.enode() ]) - .then(([clientVersion, coinbase, defaultExtraData, extraData, gasFloorTarget, hashrate, minGasPrice, netChain, netPeers, netPort, nodeName, rpcSettings, syncing, traceMode]) => { + .then(([ + clientVersion, defaultExtraData, netChain, netPort, rpcSettings, enode + ]) => { const isTest = netChain === 'morden' || netChain === 'testnet'; - this._store.dispatch(statusCollection({ + const longStatus = { clientVersion, - coinbase, defaultExtraData, - extraData, - gasFloorTarget, - hashrate, - minGasPrice, netChain, - netPeers, netPort, - nodeName, rpcSettings, - syncing, - isTest, - traceMode - })); + enode, + isTest + }; + + if (!isEqual(longStatus, this._longStatus)) { + this._store.dispatch(statusCollection(longStatus)); + this._longStatus = longStatus; + } + + nextTimeout(); }) .catch((error) => { - console.error('_pollStatus', error); + console.error('_pollLongStatus', error); + nextTimeout(250); }); - - nextTimeout(); } _pollLogs = () => { diff --git a/js/src/redux/providers/statusActions.js b/js/src/redux/providers/statusActions.js index 142cdabdc..1c175c29d 100644 --- a/js/src/redux/providers/statusActions.js +++ b/js/src/redux/providers/statusActions.js @@ -47,3 +47,10 @@ export function clearStatusLogs () { type: 'clearStatusLogs' }; } + +export function toggleStatusRefresh (refreshStatus) { + return { + type: 'toggleStatusRefresh', + refreshStatus + }; +} diff --git a/js/src/redux/providers/statusReducer.js b/js/src/redux/providers/statusReducer.js index 98bb536ae..b0450083a 100644 --- a/js/src/redux/providers/statusReducer.js +++ b/js/src/redux/providers/statusReducer.js @@ -37,12 +37,13 @@ const initialState = { max: new BigNumber(0) }, netPort: new BigNumber(0), - nodeName: '', rpcSettings: {}, syncing: false, - isApiConnected: true, - isPingConnected: true, + isConnected: false, + isConnecting: false, + isPingable: false, isTest: false, + refreshStatus: false, traceMode: undefined }; @@ -73,5 +74,10 @@ export default handleActions({ clearStatusLogs (state, action) { return Object.assign({}, state, { devLogs: [] }); + }, + + toggleStatusRefresh (state, action) { + const { refreshStatus } = action; + return Object.assign({}, state, { refreshStatus }); } }, initialState); diff --git a/js/src/views/Status/containers/StatusPage/StatusPage.js b/js/src/views/Status/containers/StatusPage/StatusPage.js index afc7d60f7..617a6486a 100644 --- a/js/src/views/Status/containers/StatusPage/StatusPage.js +++ b/js/src/views/Status/containers/StatusPage/StatusPage.js @@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import { clearStatusLogs, toggleStatusLogs } from '../../../../redux/actions'; +import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from '../../../../redux/actions'; import Debug from '../../components/Debug'; import Status from '../../components/Status'; @@ -31,6 +31,14 @@ class StatusPage extends Component { actions: PropTypes.object.isRequired } + componentWillMount () { + this.props.actions.toggleStatusRefresh(true); + } + + componentWillUnmount () { + this.props.actions.toggleStatusRefresh(false); + } + render () { return (
@@ -49,7 +57,8 @@ function mapDispatchToProps (dispatch) { return { actions: bindActionCreators({ clearStatusLogs, - toggleStatusLogs + toggleStatusLogs, + toggleStatusRefresh }, dispatch) }; }