From d15d9351da577b2c2275dcb8f4d016e4b19f9a6b Mon Sep 17 00:00:00 2001 From: Fabian Vogelsteller Date: Wed, 9 Sep 2015 21:59:01 +0200 Subject: [PATCH] add isSyncing and better way of matching batch requests --- lib/web3.js | 5 ++ lib/web3/filter.js | 12 ++-- lib/web3/formatters.js | 13 +++- lib/web3/methods/eth.js | 4 ++ lib/web3/requestmanager.js | 23 ++++--- lib/web3/syncing.js | 101 +++++++++++++++++++++++++++++++ test/contract.js | 3 + test/helpers/FakeHttpProvider.js | 25 ++++++-- test/polling.js | 2 + test/web3.eth.hashRate.js | 4 ++ test/web3.eth.isSyncing.js | 60 ++++++++++++++++++ 11 files changed, 233 insertions(+), 19 deletions(-) create mode 100644 lib/web3/syncing.js create mode 100644 test/web3.eth.isSyncing.js diff --git a/lib/web3.js b/lib/web3.js index 2d59fc6ec2d..8ec99e56a24 100644 --- a/lib/web3.js +++ b/lib/web3.js @@ -31,6 +31,7 @@ var db = require('./web3/methods/db'); var shh = require('./web3/methods/shh'); var watches = require('./web3/methods/watches'); var Filter = require('./web3/filter'); +var IsSyncing = require('./web3/syncing'); var utils = require('./utils/utils'); var formatters = require('./web3/formatters'); var RequestManager = require('./web3/requestmanager'); @@ -85,6 +86,10 @@ web3.version = {}; web3.version.api = version.version; web3.eth = {}; +web3.eth.isSyncing = function (callback) { + return new IsSyncing(callback); +}; + /*jshint maxparams:4 */ web3.eth.filter = function (fil, callback) { return new Filter(fil, watches.eth(), formatters.outputLogFormatter, callback); diff --git a/lib/web3/filter.js b/lib/web3/filter.js index 7e5274ebb64..f22b7193695 100644 --- a/lib/web3/filter.js +++ b/lib/web3/filter.js @@ -114,12 +114,14 @@ var pollFilter = function(self) { }); } - messages.forEach(function (message) { - message = self.formatter ? self.formatter(message) : message; - self.callbacks.forEach(function (callback) { - callback(null, message); + if(utils.isArray(messages)) { + messages.forEach(function (message) { + message = self.formatter ? self.formatter(message) : message; + self.callbacks.forEach(function (callback) { + callback(null, message); + }); }); - }); + } }; RequestManager.getInstance().startPolling({ diff --git a/lib/web3/formatters.js b/lib/web3/formatters.js index 1c516ae9647..16c80c644c2 100644 --- a/lib/web3/formatters.js +++ b/lib/web3/formatters.js @@ -270,6 +270,16 @@ var inputAddressFormatter = function (address) { throw 'invalid address'; }; + +var outputSyncingFormatter = function(result) { + + result.startingBlock = utils.toDecimal(result.startingBlock); + result.currentBlock = utils.toDecimal(result.currentBlock); + result.highestBlock = utils.toDecimal(result.highestBlock); + + return result; +}; + module.exports = { inputDefaultBlockNumberFormatter: inputDefaultBlockNumberFormatter, inputBlockNumberFormatter: inputBlockNumberFormatter, @@ -282,6 +292,7 @@ module.exports = { outputTransactionReceiptFormatter: outputTransactionReceiptFormatter, outputBlockFormatter: outputBlockFormatter, outputLogFormatter: outputLogFormatter, - outputPostFormatter: outputPostFormatter + outputPostFormatter: outputPostFormatter, + outputSyncingFormatter: outputSyncingFormatter }; diff --git a/lib/web3/methods/eth.js b/lib/web3/methods/eth.js index 4d359af72cd..7a557c04bb9 100644 --- a/lib/web3/methods/eth.js +++ b/lib/web3/methods/eth.js @@ -268,6 +268,10 @@ var properties = [ getter: 'eth_hashrate', outputFormatter: utils.toDecimal }), + new Property({ + name: 'syncing', + getter: 'eth_syncing' + }), new Property({ name: 'gasPrice', getter: 'eth_gasPrice', diff --git a/lib/web3/requestmanager.js b/lib/web3/requestmanager.js index f994161b232..e439127cf28 100644 --- a/lib/web3/requestmanager.js +++ b/lib/web3/requestmanager.js @@ -209,10 +209,10 @@ RequestManager.prototype.poll = function () { } var pollsData = []; - var pollsKeys = []; + var pollsIds = []; for (var key in this.polls) { pollsData.push(this.polls[key].data); - pollsKeys.push(key); + pollsIds.push(key); } if (pollsData.length === 0) { @@ -220,9 +220,18 @@ RequestManager.prototype.poll = function () { } var payload = Jsonrpc.getInstance().toBatchPayload(pollsData); + + // map the request id to they poll id + var pollsIdMap = {}; + payload.forEach(function(load, index){ + pollsIdMap[load.id] = pollsIds[index]; + }); + var self = this; this.provider.sendAsync(payload, function (error, results) { + + // TODO: console log? if (error) { return; @@ -231,12 +240,12 @@ RequestManager.prototype.poll = function () { if (!utils.isArray(results)) { throw errors.InvalidResponse(results); } - results.map(function (result, index) { - var key = pollsKeys[index]; + var id = pollsIdMap[result.id]; + // make sure the filter is still installed after arrival of the request - if (self.polls[key]) { - result.callback = self.polls[key].callback; + if (self.polls[id]) { + result.callback = self.polls[id].callback; return result; } else return false; @@ -248,8 +257,6 @@ RequestManager.prototype.poll = function () { result.callback(errors.InvalidResponse(result)); } return valid; - }).filter(function (result) { - return utils.isArray(result.result) && result.result.length > 0; }).forEach(function (result) { result.callback(null, result.result); }); diff --git a/lib/web3/syncing.js b/lib/web3/syncing.js new file mode 100644 index 00000000000..766013a0baf --- /dev/null +++ b/lib/web3/syncing.js @@ -0,0 +1,101 @@ +/* + This file is part of ethereum.js. + + ethereum.js is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + ethereum.js is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with ethereum.js. If not, see . +*/ +/** @file syncing.js + * @authors: + * Fabian Vogelsteller + * @date 2015 + */ + +var RequestManager = require('./requestmanager'); +var Method = require('./method'); +var formatters = require('./formatters'); +var utils = require('../utils/utils'); + + + +/** +Adds the callback and sets up the methods, to iterate over the results. + +@method pollSyncing +@param {Object} self +*/ +var pollSyncing = function(self) { + var lastSyncState = false; + + var onMessage = function (error, sync) { + if (error) { + return self.callbacks.forEach(function (callback) { + callback(error); + }); + } + + if(utils.isObject(sync)) + sync = self.implementation.outputFormatter(sync); + + self.callbacks.forEach(function (callback) { + if(lastSyncState !== sync) { + + // call the callback with true first so the app can stop anything, before receiving the sync data + if(!lastSyncState && utils.isObject(sync)) + callback(null, true); + + // call on the next CPU cycle, so the actions of the sync stop can be processes first + setTimeout(function() { + callback(null, sync); + }, 1); + + lastSyncState = sync; + } + }); + }; + + RequestManager.getInstance().startPolling({ + method: self.implementation.call, + params: [], + }, self.pollId, onMessage, self.stopPolling.bind(self)); + +}; + +var IsSyncing = function (callback) { + this.pollId = 'syncPoll_'+ Math.floor(Math.random() * 1000); + this.callbacks = []; + this.implementation = new Method({ + name: 'isSyncing', + call: 'eth_syncing', + params: 0, + outputFormatter: formatters.outputSyncingFormatter + }); + + this.addCallback(callback); + pollSyncing(this); + + return this; +}; + +IsSyncing.prototype.addCallback = function (callback) { + if(callback) + this.callbacks.push(callback); + return this; +}; + +IsSyncing.prototype.stopPolling = function () { + RequestManager.getInstance().stopPolling(this.pollId); + this.callbacks = []; +}; + +module.exports = IsSyncing; + diff --git a/test/contract.js b/test/contract.js index 65196b8731b..b5df4a7ca45 100644 --- a/test/contract.js +++ b/test/contract.js @@ -122,6 +122,7 @@ describe('contract', function () { assert.equal(result.args.t2, 8); res++; if (res === 2) { + event.stopWatching(); done(); } }); @@ -191,6 +192,7 @@ describe('contract', function () { assert.equal(result.args.t2, 8); res++; if (res === 2) { + event.stopWatching(); done(); } }); @@ -257,6 +259,7 @@ describe('contract', function () { assert.equal(result.args.t2, 8); res++; if (res === 2) { + event.stopWatching(); done(); } }); diff --git a/test/helpers/FakeHttpProvider.js b/test/helpers/FakeHttpProvider.js index 1e6ce125256..594787f095d 100644 --- a/test/helpers/FakeHttpProvider.js +++ b/test/helpers/FakeHttpProvider.js @@ -2,10 +2,12 @@ var chai = require('chai'); var assert = require('assert'); var utils = require('../../lib/utils/utils'); +countId = 1; + var getResponseStub = function () { return { jsonrpc: '2.0', - id: 1, + id: countId++, result: 0 }; }; @@ -13,7 +15,7 @@ var getResponseStub = function () { var getErrorStub = function () { return { jsonrpc: '2.0', - id: 1, + countId: countId++, error: { code: 1234, message: '' @@ -37,7 +39,8 @@ FakeHttpProvider.prototype.send = function (payload) { // imitate plain json object this.validation(JSON.parse(JSON.stringify(payload))); } - return this.getResponse(); + + return this.getResponse(payload); }; FakeHttpProvider.prototype.sendAsync = function (payload, callback) { @@ -47,7 +50,8 @@ FakeHttpProvider.prototype.sendAsync = function (payload, callback) { // imitate plain json object this.validation(JSON.parse(JSON.stringify(payload)), callback); } - callback(this.error, this.getResponse()); + + callback(this.error, this.getResponse(payload)); }; FakeHttpProvider.prototype.injectResponse = function (response) { @@ -72,7 +76,18 @@ FakeHttpProvider.prototype.injectBatchResults = function (results, error) { }); }; -FakeHttpProvider.prototype.getResponse = function () { +FakeHttpProvider.prototype.getResponse = function (payload) { + + if(this.response) { + if(utils.isArray(this.response)) { + this.response = this.response.map(function(response, index) { + response.id = payload[index] ? payload[index].id : countId++; + return response; + }); + } else + this.response.id = payload.id; + } + return this.response; }; diff --git a/test/polling.js b/test/polling.js index 5c31e1390f4..99d5b765630 100644 --- a/test/polling.js +++ b/test/polling.js @@ -68,6 +68,7 @@ var testPolling = function (tests) { } else { assert.equal(result, test.secondResult[0]); } + filter.stopWatching(); done(); }); @@ -102,6 +103,7 @@ var testPolling = function (tests) { } else { assert.equal(result, test.secondResult[0]); } + filter.stopWatching(); done(); }); diff --git a/test/web3.eth.hashRate.js b/test/web3.eth.hashRate.js index ec01bfad1eb..1620c5ed8f4 100644 --- a/test/web3.eth.hashRate.js +++ b/test/web3.eth.hashRate.js @@ -31,6 +31,10 @@ describe('web3.eth', function () { // then assert.strictEqual(test.formattedResult, result); + + // clear the validation + provider.injectValidation(function () {}); + web3.reset(); }); }); }); diff --git a/test/web3.eth.isSyncing.js b/test/web3.eth.isSyncing.js new file mode 100644 index 00000000000..884096fa6ce --- /dev/null +++ b/test/web3.eth.isSyncing.js @@ -0,0 +1,60 @@ +var chai = require('chai'); +var web3 = require('../index'); +var assert = chai.assert; +var FakeHttpProvider = require('./helpers/FakeHttpProvider'); + +var method = 'isSyncing'; + +var tests = [{ + args: [], + formattedArgs: [], + result: [{ + startingBlock: '0xb', + currentBlock: '0xb', + highestBlock: '0xb' + }], + formattedResult: { + startingBlock: 11, + currentBlock: 11, + highestBlock: 11 + }, + call: 'eth_syncing' +}]; + +describe('eth', function () { + describe(method, function () { + tests.forEach(function (test, index) { + it('property test: ' + index, function (done) { + + // given + var provider = new FakeHttpProvider(); + web3.setProvider(provider); + provider.injectBatchResults(test.result); + provider.injectValidation(function (payload) { + assert.equal(payload[0].jsonrpc, '2.0'); + assert.equal(payload[0].method, test.call); + assert.deepEqual(payload[0].params, test.formattedArgs); + }); + + var count = 1; + + // TODO results seem to be overwritten + + // call + var syncing = web3.eth[method](function(e, res){ + if(count === 1) { + assert.isTrue(res); + count++; + } else { + assert.deepEqual(res, test.formattedResult); + syncing.stopPolling(); + done(); + } + }); + + }); + }); + }); +}); + +