diff --git a/Cargo.lock b/Cargo.lock index b73c3e3a1..3c584e12c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -386,7 +386,7 @@ version = "1.5.0" dependencies = [ "crossbeam 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.1 (git+https://github.com/carllerche/mio)", + "mio 0.6.1 (git+https://github.com/ethcore/mio)", "parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -476,7 +476,7 @@ dependencies = [ "igd 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.1 (git+https://github.com/carllerche/mio)", + "mio 0.6.1 (git+https://github.com/ethcore/mio)", "parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rlp 0.1.0", @@ -1028,7 +1028,7 @@ dependencies = [ [[package]] name = "mio" version = "0.6.1" -source = "git+https://github.com/carllerche/mio#56f8663510196fdca04bdf7c5f4d60b24297826f" +source = "git+https://github.com/ethcore/mio#ef182bae193a9c7457cd2cf661fcaffb226e3eef" dependencies = [ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "lazycell 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1265,7 +1265,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#55741cc9850ad6dcbcb7e0a7ac16e805df85de9a" +source = "git+https://github.com/ethcore/js-precompiled.git#1099b22904fd8c6f2e0925cc7124da4d68462d4e" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2072,7 +2072,7 @@ dependencies = [ "checksum mio 0.5.1 (git+https://github.com/ethcore/mio?branch=v0.5.x)" = "" "checksum mio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a637d1ca14eacae06296a008fa7ad955347e34efcb5891cfd8ba05491a37907e" "checksum mio 0.6.0-dev (git+https://github.com/ethcore/mio?branch=timer-fix)" = "" -"checksum mio 0.6.1 (git+https://github.com/carllerche/mio)" = "" +"checksum mio 0.6.1 (git+https://github.com/ethcore/mio)" = "" "checksum miow 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d5bfc6782530ac8ace97af10a540054a37126b63b0702ddaaa243b73b5745b9a" "checksum msdos_time 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c04b68cc63a8480fb2550343695f7be72effdec953a9d4508161c3e69041c7d8" "checksum nanomsg 0.5.1 (git+https://github.com/ethcore/nanomsg.rs.git)" = "" diff --git a/js/package.json b/js/package.json index c762db3de..0543b7883 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.2.67", + "version": "0.2.68", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", diff --git a/js/src/api/transport/ws/ws.js b/js/src/api/transport/ws/ws.js index 7b214fded..c30c910e6 100644 --- a/js/src/api/transport/ws/ws.js +++ b/js/src/api/transport/ws/ws.js @@ -29,21 +29,33 @@ export default class Ws extends JsonRpcBase { this._token = token; this._messages = {}; - this._connecting = true; + this._connecting = false; + this._connected = false; this._lastError = null; - this._autoConnect = false; + this._autoConnect = true; + this._retries = 0; + this._reconnectTimeoutId = null; this._connect(); } updateToken (token) { this._token = token; - this._autoConnect = false; + this._autoConnect = true; this._connect(); } _connect () { + if (this._connecting) { + return; + } + + if (this._reconnectTimeoutId) { + window.clearTimeout(this._reconnectTimeoutId); + this._reconnectTimeoutId = null; + } + const time = parseInt(new Date().getTime() / 1000, 10); const sha3 = keccak_256(`${this._token}:${time}`); const hash = `${sha3}_${time}`; @@ -53,6 +65,7 @@ export default class Ws extends JsonRpcBase { this._ws.onopen = null; this._ws.onclose = null; this._ws.onmessage = null; + this._ws.close(); this._ws = null; } @@ -65,6 +78,27 @@ export default class Ws extends JsonRpcBase { this._ws.onopen = this._onOpen; this._ws.onclose = this._onClose; this._ws.onmessage = this._onMessage; + + // Get counts in dev mode + if (process.env.NODE_ENV === 'development') { + this._count = 0; + this._lastCount = { + timestamp: Date.now(), + count: 0 + }; + + window.setInterval(() => { + const n = this._count - this._lastCount.count; + const t = (Date.now() - this._lastCount.timestamp) / 1000; + const s = Math.round(1000 * n / t) / 1000; + + if (this._debug) { + console.log('::parityWS', `speed: ${s} req/s`, `count: ${this._count}`); + } + }, 5000); + + window._parityWS = this; + } } _onOpen = (event) => { @@ -72,6 +106,7 @@ export default class Ws extends JsonRpcBase { this._connected = true; this._connecting = false; this._autoConnect = true; + this._retries = 0; Object.keys(this._messages) .filter((id) => this._messages[id].queued) @@ -79,18 +114,39 @@ export default class Ws extends JsonRpcBase { } _onClose = (event) => { - console.log('ws:onClose', event); this._connected = false; this._connecting = false; + this._lastError = event; + if (this._autoConnect) { - setTimeout(() => this._connect(), 500); + const timeout = this.retryTimeout; + + const time = timeout < 1000 + ? Math.round(timeout) + 'ms' + : (Math.round(timeout / 10) / 100) + 's'; + + console.log('ws:onClose', `trying again in ${time}...`); + + this._reconnectTimeoutId = setTimeout(() => { + this._connect(); + }, timeout); + + return; } + + console.log('ws:onClose', event); } _onError = (event) => { - console.error('ws:onError', event); - this._lastError = event; + // Only print error if the WS is connected + // ie. don't print if error == closed + window.setTimeout(() => { + if (this._connected) { + console.error('ws:onError', event); + this._lastError = event; + } + }, 50); } _onMessage = (event) => { @@ -127,11 +183,16 @@ export default class Ws extends JsonRpcBase { _send = (id) => { const message = this._messages[id]; - message.queued = !this._connected; - if (this._connected) { - this._ws.send(message.json); + if (process.env.NODE_ENV === 'development') { + this._count++; + } + + return this._ws.send(message.json); } + + message.queued = !this._connected; + message.timestamp = Date.now(); } execute (method, ...params) { @@ -159,4 +220,27 @@ export default class Ws extends JsonRpcBase { get lastError () { return this._lastError; } + + /** + * Exponential Timeout for Retries + * + * @see http://dthain.blogspot.de/2009/02/exponential-backoff-in-distributed.html + */ + get retryTimeout () { + // R between 1 and 2 + const R = Math.random() + 1; + // Initial timeout (100ms) + const T = 100; + // Exponential Factor + const F = 2; + // Max timeout (4s) + const M = 4000; + // Current number of retries + const N = this._retries; + + // Increase retries number + this._retries++; + + return Math.min(R * T * Math.pow(F, N), M); + } } diff --git a/js/src/contracts/registry.js b/js/src/contracts/registry.js index d52b20718..2f61f7f4a 100644 --- a/js/src/contracts/registry.js +++ b/js/src/contracts/registry.js @@ -21,25 +21,39 @@ export default class Registry { this._api = api; this._contracts = []; this._instance = null; + this._fetching = false; + this._queue = []; this.getInstance(); } getInstance () { - return new Promise((resolve, reject) => { - if (this._instance) { - resolve(this._instance); - return; - } + if (this._instance) { + return Promise.resolve(this._instance); + } - this._api.parity - .registryAddress() - .then((address) => { - this._instance = this._api.newContract(abis.registry, address).instance; - resolve(this._instance); - }) - .catch(reject); - }); + if (this._fetching) { + return new Promise((resolve) => { + this._queue.push({ resolve }); + }); + } + + this._fetching = true; + + return this._api.parity + .registryAddress() + .then((address) => { + this._fetching = false; + this._instance = this._api.newContract(abis.registry, address).instance; + + this._queue.forEach((queued) => { + queued.resolve(this._instance); + }); + + this._queue = []; + + return this._instance; + }); } getContract (_name) { diff --git a/js/src/redux/providers/balances.js b/js/src/redux/providers/balances.js index bcc8eca0a..9e9c0a481 100644 --- a/js/src/redux/providers/balances.js +++ b/js/src/redux/providers/balances.js @@ -14,9 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import { throttle } from 'lodash'; + import { getBalances, getTokens } from './balancesActions'; import { setAddressImage } from './imagesActions'; +import Contracts from '../../contracts'; import * as abis from '../../contracts/abi'; import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png'; @@ -37,11 +40,20 @@ export default class Balances { this._accountsInfo = null; this._tokenreg = null; + this._fetchingBalances = false; this._fetchingTokens = false; this._fetchedTokens = false; this._tokenregSubId = null; this._tokenregMetaSubId = null; + + // Throttled `retrieveTokens` function + // that gets called max once every 20s + this._throttledRetrieveTokens = throttle( + this._retrieveTokens, + 20 * 1000, + { trailing: true } + ); } start () { @@ -72,6 +84,15 @@ export default class Balances { return console.warn('_subscribeBlockNumber', error); } + const { syncing } = this._store.getState().nodeStatus; + + // If syncing, only retrieve balances once every + // few seconds + if (syncing) { + return this._throttledRetrieveTokens(); + } + + this._throttledRetrieveTokens.cancel(); this._retrieveTokens(); }) .catch((error) => { @@ -84,15 +105,9 @@ export default class Balances { return Promise.resolve(this._tokenreg); } - return this._api.parity - .registryAddress() - .then((registryAddress) => { - const registry = this._api.newContract(abis.registry, registryAddress); - - return registry.instance.getAddress.call({}, [this._api.util.sha3('tokenreg'), 'A']); - }) - .then((tokenregAddress) => { - const tokenreg = this._api.newContract(abis.tokenreg, tokenregAddress); + return Contracts.get().tokenReg + .getContract() + .then((tokenreg) => { this._tokenreg = tokenreg; this.attachToTokens(); @@ -140,10 +155,16 @@ export default class Balances { } _retrieveBalances () { + if (this._fetchingBalances) { + return; + } + if (!this._accountsInfo) { return; } + this._fetchingBalances = true; + const addresses = Object .keys(this._accountsInfo) .filter((address) => { @@ -161,9 +182,11 @@ export default class Balances { }); this._store.dispatch(getBalances(this._balances)); + this._fetchingBalances = false; }) .catch((error) => { console.warn('_retrieveBalances', error); + this._fetchingBalances = false; }); } diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index 50b28a5ac..2153d1ddf 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -71,8 +71,8 @@ export default class Status { * @see src/views/Connection/connection.js */ _shouldPing = () => { - const { isConnected, isConnecting } = this._apiStatus; - return isConnecting || !isConnected; + const { isConnected } = this._apiStatus; + return !isConnected; } _stopPollPing = () => { @@ -119,7 +119,7 @@ export default class Status { _pollStatus = () => { const nextTimeout = (timeout = 1000) => { - setTimeout(this._pollStatus, timeout); + setTimeout(() => this._pollStatus(), timeout); }; const { isConnected, isConnecting, needsToken, secureToken } = this._api; @@ -134,7 +134,8 @@ export default class Status { const gotReconnected = !this._apiStatus.isConnected && apiStatus.isConnected; if (gotReconnected) { - this._pollLongStatus(); + this._pollLongStatus(true); + this._store.dispatch(statusCollection({ isPingable: true })); } if (!isEqual(apiStatus, this._apiStatus)) { @@ -175,13 +176,12 @@ export default class Status { this._store.dispatch(statusCollection(status)); this._status = status; } - - nextTimeout(); }) .catch((error) => { console.error('_pollStatus', error); - nextTimeout(250); }); + + nextTimeout(); } /** @@ -223,7 +223,11 @@ export default class Status { * fetched every 30s just in case, and whenever * the client got reconnected. */ - _pollLongStatus = () => { + _pollLongStatus = (newConnection = false) => { + if (!this._api.isConnected) { + return; + } + const nextTimeout = (timeout = 30000) => { if (this._longStatusTimeoutId) { clearTimeout(this._longStatusTimeoutId); @@ -242,7 +246,7 @@ export default class Status { this._api.parity.netChain(), this._api.parity.netPort(), this._api.parity.rpcSettings(), - this._api.parity.enode() + newConnection ? Promise.resolve(null) : this._api.parity.enode() ]) .then(([ clientVersion, defaultExtraData, netChain, netPort, rpcSettings, enode @@ -255,21 +259,23 @@ export default class Status { netChain, netPort, rpcSettings, - enode, isTest }; + if (enode) { + longStatus.enode = enode; + } + if (!isEqual(longStatus, this._longStatus)) { this._store.dispatch(statusCollection(longStatus)); this._longStatus = longStatus; } - - nextTimeout(); }) .catch((error) => { console.error('_pollLongStatus', error); - nextTimeout(250); }); + + nextTimeout(newConnection ? 5000 : 30000); } _pollLogs = () => { diff --git a/js/src/redux/providers/statusReducer.js b/js/src/redux/providers/statusReducer.js index 07fa993b9..f0ef0cb1b 100644 --- a/js/src/redux/providers/statusReducer.js +++ b/js/src/redux/providers/statusReducer.js @@ -39,7 +39,7 @@ const initialState = { }, netPort: new BigNumber(0), rpcSettings: {}, - syncing: false, + syncing: true, isConnected: false, isConnecting: false, isPingable: false, diff --git a/js/src/secureApi.js b/js/src/secureApi.js index 3e1d9eab0..af62da2cf 100644 --- a/js/src/secureApi.js +++ b/js/src/secureApi.js @@ -25,12 +25,13 @@ export default class SecureApi extends Api { this._isConnecting = true; this._connectState = sysuiToken === 'initial' ? 1 : 0; this._needsToken = false; - this._nextToken = nextToken; this._dappsPort = 8080; this._dappsInterface = null; this._signerPort = 8180; + this._followConnectionTimeoutId = null; - console.log('SecureApi:constructor', sysuiToken); + // Try tokens from localstorage, then from hash + this._tokensToTry = [ sysuiToken, nextToken ].filter((t) => t && t.length); this._followConnection(); } @@ -40,15 +41,30 @@ export default class SecureApi extends Api { console.log('SecureApi:setToken', this._transport.token); } + _checkNodeUp () { + return fetch('/', { method: 'HEAD' }) + .then( + (r) => r.status === 200, + () => false + ) + .catch(() => false); + } + _followConnection = () => { const nextTick = () => { - setTimeout(() => this._followConnection(), 250); + if (this._followConnectionTimeoutId) { + clearTimeout(this._followConnectionTimeoutId); + } + + this._followConnectionTimeoutId = setTimeout(() => this._followConnection(), 250); }; + const setManual = () => { this._connectState = 100; this._needsToken = true; this._isConnecting = false; }; + const lastError = this._transport.lastError; const isConnected = this._transport.isConnected; @@ -58,11 +74,23 @@ export default class SecureApi extends Api { if (isConnected) { return this.connectSuccess(); } else if (lastError) { - const nextToken = this._nextToken || 'initial'; - const nextState = this._nextToken ? 0 : 1; + return this + ._checkNodeUp() + .then((isNodeUp) => { + const nextToken = this._tokensToTry[0] || 'initial'; + const nextState = nextToken !== 'initial' ? 0 : 1; - this._nextToken = null; - this.updateToken(nextToken, nextState); + // If previous token was wrong (error while node up), delete it + if (isNodeUp) { + this._tokensToTry = this._tokensToTry.slice(1); + } + + if (nextToken !== this._transport.token) { + this.updateToken(nextToken, nextState); + } + + nextTick(); + }); } break; diff --git a/js/webpack.config.js b/js/webpack.config.js index 91a9caf2e..b70123aff 100644 --- a/js/webpack.config.js +++ b/js/webpack.config.js @@ -197,30 +197,42 @@ module.exports = { historyApiFallback: false, quiet: false, hot: !isProd, - proxy: { - '/api/*': { + proxy: [ + { + context: (pathname, req) => { + return pathname === '/' && req.method === 'HEAD'; + }, + target: 'http://127.0.0.1:8180', + changeOrigin: true, + autoRewrite: true + }, + { + context: '/api/*', target: 'http://127.0.0.1:8080', changeOrigin: true, autoRewrite: true }, - '/app/*': { + { + context: '/app/*', target: 'http://127.0.0.1:8080', changeOrigin: true, pathRewrite: { '^/app': '' } }, - '/parity-utils/*': { + { + context: '/parity-utils/*', target: 'http://127.0.0.1:3000', changeOrigin: true, pathRewrite: { '^/parity-utils': '' } }, - '/rpc/*': { + { + context: '/rpc/*', target: 'http://localhost:8080', changeOrigin: true } - } + ] } }; diff --git a/util/io/Cargo.toml b/util/io/Cargo.toml index 8c7f46cb9..0a7adaf56 100644 --- a/util/io/Cargo.toml +++ b/util/io/Cargo.toml @@ -7,7 +7,7 @@ version = "1.5.0" authors = ["Ethcore "] [dependencies] -mio = { git = "https://github.com/carllerche/mio" } +mio = { git = "https://github.com/ethcore/mio" } crossbeam = "0.2" parking_lot = "0.3" log = "0.3" diff --git a/util/network/Cargo.toml b/util/network/Cargo.toml index 02a54375c..fe1938305 100644 --- a/util/network/Cargo.toml +++ b/util/network/Cargo.toml @@ -8,7 +8,7 @@ authors = ["Ethcore "] [dependencies] log = "0.3" -mio = { git = "https://github.com/carllerche/mio" } +mio = { git = "https://github.com/ethcore/mio" } bytes = "0.3.0" rand = "0.3.12" time = "0.1.34"