From a22469ade57d68ba1170b72b55e4eee41e0021d7 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Wed, 23 Nov 2016 13:56:48 +0100 Subject: [PATCH 1/5] Fix contract method name --- js/src/api/contract/contract.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/api/contract/contract.js b/js/src/api/contract/contract.js index f7cd727f8..6fe497551 100644 --- a/js/src/api/contract/contract.js +++ b/js/src/api/contract/contract.js @@ -389,7 +389,7 @@ export default class Contract { const subscriptions = Object.values(this._subscriptions) .filter((s) => s.options.toBlock && s.options.toBlock === 'pending'); - const timeout = () => setTimeout(() => this._subscribeFromPendings(), 1000); + const timeout = () => setTimeout(() => this._subscribeToPendings(), 1000); this._sendSubscriptionChanges(subscriptions) .then(() => { From f800dd1eee0fda4ba93a828f84e4f6de3fbb587a Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Wed, 23 Nov 2016 17:37:04 +0100 Subject: [PATCH 2/5] WebSocket Improvments #3544 --- js/src/api/transport/ws/ws.js | 134 ++++++++++++++++++++++++++--- js/src/contracts/registry.js | 40 ++++++--- js/src/redux/providers/balances.js | 13 +-- js/src/redux/providers/status.js | 34 +++++--- js/src/secureApi.js | 27 ++++-- 5 files changed, 193 insertions(+), 55 deletions(-) diff --git a/js/src/api/transport/ws/ws.js b/js/src/api/transport/ws/ws.js index 7b214fded..6edbef37f 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,42 @@ 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; + } + } + + _checkNodeUp () { + const url = process.env.PARITY_URL || window.location.host; + + return fetch( + `http://${url}/api/ping`, + { method: 'HEAD' } + ) + .then((r) => { + return r.status === 200; + }, () => { + return false; + }) + .catch(() => false); } _onOpen = (event) => { @@ -72,6 +121,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 +129,50 @@ export default class Ws extends JsonRpcBase { } _onClose = (event) => { - console.log('ws:onClose', event); this._connected = false; this._connecting = false; - if (this._autoConnect) { - setTimeout(() => this._connect(), 500); - } + this._checkNodeUp() + .then((up) => { + // If the connection has been closed and the node + // is up, it means we have a wrong token + // Event code 1006 for WS means there is an error + // (not just closed by server) + if (up && event.code === 1006) { + event.status = 403; + } + + this._lastError = event; + + if (this._autoConnect) { + 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 +209,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 +246,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..6cb15eaa9 100644 --- a/js/src/redux/providers/balances.js +++ b/js/src/redux/providers/balances.js @@ -17,6 +17,7 @@ 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'; @@ -84,15 +85,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(); diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index 50b28a5ac..7bae14bc8 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 = () => { @@ -102,7 +102,7 @@ export default class Status { }, timeout); }; - fetch('/', { method: 'HEAD' }) + fetch('/api/ping', { method: 'HEAD' }) .then((response) => dispatch(!!response.ok)) .catch(() => dispatch(false)); } @@ -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/secureApi.js b/js/src/secureApi.js index 3e1d9eab0..5a5c05f51 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 hash, then from localstorage + this._tokensToTry = [ nextToken, sysuiToken ].filter((t) => t && t.length); this._followConnection(); } @@ -42,13 +43,19 @@ export default class SecureApi extends Api { _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 +65,17 @@ 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; + const nextToken = this._tokensToTry[0] || 'initial'; + const nextState = nextToken !== 'initial' ? 0 : 1; - this._nextToken = null; - this.updateToken(nextToken, nextState); + // If previous token was wrong, delete it + if (lastError.status === 403) { + this._tokensToTry = this._tokensToTry.slice(1); + } + + if (nextToken !== this._transport.token) { + this.updateToken(nextToken, nextState); + } } break; From ab212fcdb5b31df052178e2637c8c1c47e5c55e2 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Wed, 23 Nov 2016 17:51:08 +0100 Subject: [PATCH 3/5] Localstorage SignerToken first #3587 --- js/src/secureApi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/secureApi.js b/js/src/secureApi.js index 5a5c05f51..cb011f02a 100644 --- a/js/src/secureApi.js +++ b/js/src/secureApi.js @@ -31,7 +31,7 @@ export default class SecureApi extends Api { this._followConnectionTimeoutId = null; // Try tokens from hash, then from localstorage - this._tokensToTry = [ nextToken, sysuiToken ].filter((t) => t && t.length); + this._tokensToTry = [ sysuiToken, nextToken ].filter((t) => t && t.length); this._followConnection(); } From bb6fe164784269b14ad41f3a3aa343738d4eeb4a Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Wed, 23 Nov 2016 18:08:53 +0100 Subject: [PATCH 4/5] Fixed ping and comment #3587 --- js/src/api/transport/ws/ws.js | 7 +------ js/src/redux/providers/status.js | 2 +- js/src/secureApi.js | 2 +- js/webpack.config.js | 24 ++++++++++++++++++------ 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/js/src/api/transport/ws/ws.js b/js/src/api/transport/ws/ws.js index 6edbef37f..934daa5ad 100644 --- a/js/src/api/transport/ws/ws.js +++ b/js/src/api/transport/ws/ws.js @@ -102,12 +102,7 @@ export default class Ws extends JsonRpcBase { } _checkNodeUp () { - const url = process.env.PARITY_URL || window.location.host; - - return fetch( - `http://${url}/api/ping`, - { method: 'HEAD' } - ) + return fetch('/', { method: 'HEAD' }) .then((r) => { return r.status === 200; }, () => { diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index 7bae14bc8..2153d1ddf 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -102,7 +102,7 @@ export default class Status { }, timeout); }; - fetch('/api/ping', { method: 'HEAD' }) + fetch('/', { method: 'HEAD' }) .then((response) => dispatch(!!response.ok)) .catch(() => dispatch(false)); } diff --git a/js/src/secureApi.js b/js/src/secureApi.js index cb011f02a..1b86b48f5 100644 --- a/js/src/secureApi.js +++ b/js/src/secureApi.js @@ -30,7 +30,7 @@ export default class SecureApi extends Api { this._signerPort = 8180; this._followConnectionTimeoutId = null; - // Try tokens from hash, then from localstorage + // Try tokens from localstorage, then from hash this._tokensToTry = [ sysuiToken, nextToken ].filter((t) => t && t.length); this._followConnection(); 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 } - } + ] } }; From 0c3d87f0d3af32b41d69f09acd6598a09839e269 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Wed, 23 Nov 2016 18:20:18 +0100 Subject: [PATCH 5/5] Removed check node up from WS #3587 --- js/src/api/transport/ws/ws.js | 47 ++++++++++------------------------- js/src/secureApi.js | 33 +++++++++++++++++------- 2 files changed, 37 insertions(+), 43 deletions(-) diff --git a/js/src/api/transport/ws/ws.js b/js/src/api/transport/ws/ws.js index 934daa5ad..c30c910e6 100644 --- a/js/src/api/transport/ws/ws.js +++ b/js/src/api/transport/ws/ws.js @@ -101,16 +101,6 @@ export default class Ws extends JsonRpcBase { } } - _checkNodeUp () { - return fetch('/', { method: 'HEAD' }) - .then((r) => { - return r.status === 200; - }, () => { - return false; - }) - .catch(() => false); - } - _onOpen = (event) => { console.log('ws:onOpen', event); this._connected = true; @@ -127,36 +117,25 @@ export default class Ws extends JsonRpcBase { this._connected = false; this._connecting = false; - this._checkNodeUp() - .then((up) => { - // If the connection has been closed and the node - // is up, it means we have a wrong token - // Event code 1006 for WS means there is an error - // (not just closed by server) - if (up && event.code === 1006) { - event.status = 403; - } + this._lastError = event; - this._lastError = event; + if (this._autoConnect) { + const timeout = this.retryTimeout; - if (this._autoConnect) { - const timeout = this.retryTimeout; + const time = timeout < 1000 + ? Math.round(timeout) + 'ms' + : (Math.round(timeout / 10) / 100) + 's'; - const time = timeout < 1000 - ? Math.round(timeout) + 'ms' - : (Math.round(timeout / 10) / 100) + 's'; + console.log('ws:onClose', `trying again in ${time}...`); - console.log('ws:onClose', `trying again in ${time}...`); + this._reconnectTimeoutId = setTimeout(() => { + this._connect(); + }, timeout); - this._reconnectTimeoutId = setTimeout(() => { - this._connect(); - }, timeout); + return; + } - return; - } - - console.log('ws:onClose', event); - }); + console.log('ws:onClose', event); } _onError = (event) => { diff --git a/js/src/secureApi.js b/js/src/secureApi.js index 1b86b48f5..af62da2cf 100644 --- a/js/src/secureApi.js +++ b/js/src/secureApi.js @@ -41,6 +41,15 @@ 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 = () => { if (this._followConnectionTimeoutId) { @@ -65,17 +74,23 @@ export default class SecureApi extends Api { if (isConnected) { return this.connectSuccess(); } else if (lastError) { - const nextToken = this._tokensToTry[0] || 'initial'; - const nextState = nextToken !== 'initial' ? 0 : 1; + return this + ._checkNodeUp() + .then((isNodeUp) => { + const nextToken = this._tokensToTry[0] || 'initial'; + const nextState = nextToken !== 'initial' ? 0 : 1; - // If previous token was wrong, delete it - if (lastError.status === 403) { - this._tokensToTry = this._tokensToTry.slice(1); - } + // 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); - } + if (nextToken !== this._transport.token) { + this.updateToken(nextToken, nextState); + } + + nextTick(); + }); } break;