diff --git a/docs/include_package-core.rst b/docs/include_package-core.rst index 55477806d2d..b46caba9c66 100644 --- a/docs/include_package-core.rst +++ b/docs/include_package-core.rst @@ -13,7 +13,8 @@ setProvider Will change the provider for its module. -.. note:: When called on the umbrella package ``web3`` it will also set the provider for all sub modules ``web3.eth``, ``web3.shh``, etc EXCEPT ``web3.bzz`` which needs a separate provider at all times. +.. note:: + When called on the umbrella package ``web3`` it will also set the provider for all sub modules ``web3.eth``, ``web3.shh``, etc EXCEPT ``web3.bzz`` which needs a separate provider at all times. ---------- Parameters @@ -98,6 +99,75 @@ Example // on windows the path is: "\\\\.\\pipe\\geth.ipc" // on linux the path is: "/users/myuser/.ethereum/geth.ipc" +------------- +Configuration +------------- + +.. code-block:: javascript + + // ==== + // Http + // ==== + + var Web3HttpProvider = require('web3-providers-http'); + + var options = { + keepAlive: true, + withCredentials: false, + timeout: 20000, // ms + headers: [ + { + name: 'Access-Control-Allow-Origin', + value: '*' + }, + { + ... + } + ], + agent: { + http: http.Agent(...), + baseUrl: '' + } + }; + + var provider = new Web3HttpProvider('http://localhost:8545', options); + + // ========== + // Websockets + // ========== + + var Web3WsProvider = require('web3-providers-ws'); + + var options = { + timeout: 30000, // ms + + // Useful for credentialed urls, e.g: ws://username:password@localhost:8546 + headers: { + authorization: 'Basic username:password' + }, + + // Useful if requests result are large + clientConfig: { + maxReceivedFrameSize: 100000000, // bytes - default: 1MiB + maxReceivedMessageSize: 100000000, // bytes - default: 8MiB + }, + + // Enable auto reconnection + reconnect: { + auto: true, + delay: 5000, // ms + maxAttempts: 5, + onTimeout: false + } + }; + + var ws = new Web3WsProvider('ws://localhost:8546', options); + + +More information for the Http and Websocket provider modules can be found here: + + - `HttpProvider `_ + - `WebsocketProvider `_ ------------------------------------------------------------------------------ diff --git a/package.json b/package.json index 1176393fe60..e71b3031a75 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "docs": "cd docs; make html;", "lint": "jshint *.js packages", "test": "mocha --grep E2E --invert; jshint *.js packages", - "test:unit": "nyc --no-clean --silent _mocha -- -R spec --grep E2E --invert", + "test:unit": "nyc --no-clean --silent _mocha -- -R spec --grep E2E --invert --exit", "test:cov": "npm run cov:clean; npm run test:unit; npm run test:e2e:clients; npm run cov:html", "dtslint": "lerna run dtslint", "depcheck": "lerna exec dependency-check -- --missing --verbose .", @@ -134,6 +134,7 @@ "karma-spec-reporter": "0.0.32", "lerna": "^3.18.3", "mocha": "^6.2.1", + "pify": "^4.0.1", "nyc": "^14.1.1", "puppeteer": "^1.20.0", "sandboxed-module": "^2.0.3", diff --git a/packages/web3-core-helpers/src/errors.js b/packages/web3-core-helpers/src/errors.js index 1a9ea5f8a6f..d6bb1d22c4d 100644 --- a/packages/web3-core-helpers/src/errors.js +++ b/packages/web3-core-helpers/src/errors.js @@ -31,8 +31,8 @@ module.exports = { InvalidNumberOfParams: function (got, expected, method) { return new Error('Invalid number of parameters for "'+ method +'". Got '+ got +' expected '+ expected +'!'); }, - InvalidConnection: function (host){ - return new Error('CONNECTION ERROR: Couldn\'t connect to node '+ host +'.'); + InvalidConnection: function (host, event){ + return this.ConnectionError('CONNECTION ERROR: Couldn\'t connect to node '+ host +'.', event); }, InvalidProvider: function () { return new Error('Provider not set or invalid'); @@ -44,6 +44,36 @@ module.exports = { ConnectionTimeout: function (ms){ return new Error('CONNECTION TIMEOUT: timeout of ' + ms + ' ms achived'); }, + ConnectionNotOpenError: function (event){ + return this.ConnectionError('connection not open on send()', event); + }, + ConnectionCloseError: function (event){ + if (typeof event === 'object' && event.code && event.reason) { + return this.ConnectionError( + 'CONNECTION ERROR: The connection got closed with ' + + 'the close code `' + event.code + '` and the following ' + + 'reason string `' + event.reason + '`', + event + ); + } + + return new Error('CONNECTION ERROR: The connection closed unexpectedly'); + }, + MaxAttemptsReachedOnReconnectingError: function (){ + return new Error('Maximum number of reconnect attempts reached!'); + }, + PendingRequestsOnReconnectingError: function (){ + return new Error('CONNECTION ERROR: Provider started to reconnect before the response got received!'); + }, + ConnectionError: function (msg, event){ + const error = new Error(msg); + if (event) { + error.code = event.code; + error.reason = event.reason; + } + + return error; + }, RevertInstructionError: function(reason, signature) { var error = new Error('Your request got reverted with the following reason string: ' + reason); error.reason = reason; diff --git a/packages/web3-core-helpers/types/index.d.ts b/packages/web3-core-helpers/types/index.d.ts index b969f5d521c..914c5d2e465 100644 --- a/packages/web3-core-helpers/types/index.d.ts +++ b/packages/web3-core-helpers/types/index.d.ts @@ -64,10 +64,15 @@ export class errors { expected: number, method: string ): Error; - static InvalidConnection(host: string): Error; + static InvalidConnection(host: string, event?: WebSocketEvent): ConnectionError; static InvalidProvider(): Error; static InvalidResponse(result: Error): Error; static ConnectionTimeout(ms: string): Error; + static ConnectionNotOpenError(): Error; + static ConnectionCloseError(event: WebSocketEvent | boolean): Error | ConnectionError; + static MaxAttemptsReachedOnReconnectingError(): Error; + static PendingRequestsOnReconnectingError(): Error; + static ConnectionError(msg: string, event?: WebSocketEvent): ConnectionError; static RevertInstructionError(reason: string, signature: string): RevertInstructionError static TransactionRevertInstructionError(reason: string, signature: string, receipt: object): TransactionRevertInstructionError static TransactionError(message: string, receipt: object): TransactionError @@ -82,13 +87,11 @@ export class WebsocketProviderBase { isConnecting(): boolean; - responseCallbacks: any; - notificationCallbacks: any; + requestQueue: Map; + responseQueue: Map; connected: boolean; connection: any; - addDefaultEvents(): void; - supportsSubscriptions(): boolean; send( @@ -107,6 +110,10 @@ export class WebsocketProviderBase { reset(): void; disconnect(code: number, reason: string): void; + + connect(): void; + + reconnect(): void; } export class IpcProviderBase { @@ -183,6 +190,19 @@ export interface WebsocketProviderOptions { clientConfig?: string; requestOptions?: any; origin?: string; + reconnect?: ReconnectOptions; +} + +export interface ReconnectOptions { + auto?: boolean; + delay?: number; + maxAttempts?: number; + onTimeout?: boolean; +} + +export interface RequestItem { + payload: JsonRpcPayload; + callback: (error: any, result: any) => void; } export interface JsonRpcPayload { @@ -212,3 +232,13 @@ export interface TransactionRevertInstructionError extends Error { export interface TransactionError extends Error { receipt: object; } + +export interface ConnectionError extends Error { + code: string | undefined; + reason: string | undefined; +} + +export interface WebSocketEvent { + code?: number; + reason?: string; +} diff --git a/packages/web3-core-helpers/types/tests/errors-test.ts b/packages/web3-core-helpers/types/tests/errors-test.ts index 8dc6725900a..5f00b754684 100644 --- a/packages/web3-core-helpers/types/tests/errors-test.ts +++ b/packages/web3-core-helpers/types/tests/errors-test.ts @@ -17,7 +17,7 @@ * @date 2019 */ -import { errors } from 'web3-core-helpers'; +import { errors, WebSocketEvent } from 'web3-core-helpers'; // $ExpectType Error errors.ErrorResponse(new Error('hey')); @@ -25,7 +25,7 @@ errors.ErrorResponse(new Error('hey')); // $ExpectType Error errors.InvalidNumberOfParams(1, 3, 'method'); -// $ExpectType Error +// $ExpectType ConnectionError errors.InvalidConnection('https://localhost:2345432'); // $ExpectType Error @@ -37,8 +37,42 @@ errors.InvalidResponse(new Error('hey')); // $ExpectType Error errors.ConnectionTimeout('timeout'); +// $ExpectType Error +errors.ConnectionNotOpenError(); + +// $ExpectType Error +errors.MaxAttemptsReachedOnReconnectingError(); + +// $ExpectType Error +errors.PendingRequestsOnReconnectingError(); + +const event: WebSocketEvent = {code: 100, reason: 'reason'}; +// $ExpectType ConnectionError +errors.ConnectionError('msg', event); + +// $ExpectType Error | ConnectionError +errors.ConnectionCloseError(event); + +// $ExpectType Error | ConnectionError +errors.ConnectionCloseError(true); + // $ExpectType RevertInstructionError errors.RevertInstructionError('reason', 'signature'); // $ExpectType TransactionRevertInstructionError errors.TransactionRevertInstructionError('reason', 'signature', {}); + +// $ExpectType TransactionError +errors.TransactionError('reason', {}); + +// $ExpectType TransactionError +errors.NoContractAddressFoundError({}); + +// $ExpectType TransactionError +errors.ContractCodeNotStoredError({}); + +// $ExpectType TransactionError +errors.TransactionRevertedWithoutReasonError({}); + +// $ExpectType TransactionError +errors.TransactionOutOfGasError({}); diff --git a/packages/web3-core-requestmanager/src/index.js b/packages/web3-core-requestmanager/src/index.js index 4bff56b5ea3..b91bdb83338 100644 --- a/packages/web3-core-requestmanager/src/index.js +++ b/packages/web3-core-requestmanager/src/index.js @@ -29,24 +29,26 @@ var Jsonrpc = require('./jsonrpc.js'); var BatchManager = require('./batch.js'); var givenProvider = require('./givenProvider.js'); - - - /** +/** * It's responsible for passing messages to providers * It's also responsible for polling the ethereum node for incoming messages * Default poll timeout is 1 second * Singleton + * + * @param {string|Object}provider + * @param {Net.Socket} net + * + * @constructor */ -var RequestManager = function RequestManager(provider) { +var RequestManager = function RequestManager(provider, net) { this.provider = null; this.providers = RequestManager.providers; - this.setProvider(provider); - this.subscriptions = {}; + this.setProvider(provider, net); + this.subscriptions = new Map(); }; - RequestManager.givenProvider = givenProvider; RequestManager.providers = { @@ -56,65 +58,88 @@ RequestManager.providers = { }; - /** * Should be used to set provider of request manager * * @method setProvider - * @param {Object} p + * + * @param {Object} provider + * @param {net.Socket} net + * + * @returns void */ -RequestManager.prototype.setProvider = function (p, net) { +RequestManager.prototype.setProvider = function (provider, net) { var _this = this; // autodetect provider - if(p && typeof p === 'string' && this.providers) { + if (provider && typeof provider === 'string' && this.providers) { // HTTP - if(/^http(s)?:\/\//i.test(p)) { - p = new this.providers.HttpProvider(p); + if (/^http(s)?:\/\//i.test(provider)) { + provider = new this.providers.HttpProvider(provider); // WS - } else if(/^ws(s)?:\/\//i.test(p)) { - p = new this.providers.WebsocketProvider(p); + } else if (/^ws(s)?:\/\//i.test(provider)) { + provider = new this.providers.WebsocketProvider(provider); // IPC - } else if(p && typeof net === 'object' && typeof net.connect === 'function') { - p = new this.providers.IpcProvider(p, net); + } else if (provider && typeof net === 'object' && typeof net.connect === 'function') { + provider = new this.providers.IpcProvider(provider, net); - } else if(p) { - throw new Error('Can\'t autodetect provider for "'+ p +'"'); + } else if (provider) { + throw new Error('Can\'t autodetect provider for "' + provider + '"'); } } + // reset the old one before changing, if still connected if(this.provider && this.provider.connected) this.clearSubscriptions(); - - this.provider = p || null; + this.provider = provider || null; // listen to incoming notifications - if(this.provider && this.provider.on) { - this.provider.on('data', function requestManagerNotification(result, deprecatedResult){ + if (this.provider && this.provider.on) { + this.provider.on('data', function data(result, deprecatedResult) { result = result || deprecatedResult; // this is for possible old providers, which may had the error first handler // check for result.method, to prevent old providers errors to pass as result - if(result.method && _this.subscriptions[result.params.subscription] && _this.subscriptions[result.params.subscription].callback) { - _this.subscriptions[result.params.subscription].callback(null, result.params.result); + if (result.method && _this.subscriptions.has(result.params.subscription)) { + _this.subscriptions.get(result.params.subscription).callback(null, result.params.result); } }); - // TODO add error, end, timeout, connect?? - // this.provider.on('error', function requestManagerNotification(result){ - // Object.keys(_this.subscriptions).forEach(function(id){ - // if(_this.subscriptions[id].callback) - // _this.subscriptions[id].callback(err); - // }); - // } + + // resubscribe if the provider has reconnected + this.provider.on('connect', function connect() { + _this.subscriptions.forEach(function (subscription) { + subscription.subscription.resubscribe(); + }); + }); + + // notify all subscriptions about the error condition + this.provider.on('error', function error(error) { + _this.subscriptions.forEach(function (subscription) { + subscription.callback(error); + }); + }); + + // notify all subscriptions about bad close conditions + this.provider.on('close', function close(event) { + if (!_this._isCleanCloseEvent(event) || _this._isIpcCloseError(event)){ + _this.subscriptions.forEach(function (subscription) { + subscription.callback(errors.ConnectionCloseError(event)); + _this.subscriptions.delete(subscription.subscription.id); + }); + } + }); + + // TODO add end, timeout?? } }; - /** + * TODO: This method should be implemented with a Promise instead of a callback + * * Should be used to asynchronously send request * * @method sendAsync @@ -122,7 +147,8 @@ RequestManager.prototype.setProvider = function (p, net) { * @param {Function} callback */ RequestManager.prototype.send = function (data, callback) { - callback = callback || function(){}; + callback = callback || function () { + }; if (!this.provider) { return callback(errors.InvalidProvider()); @@ -149,10 +175,12 @@ RequestManager.prototype.send = function (data, callback) { }; /** + * TODO: This method should be implemented with a Promise instead of a callback + * * Should be called to asynchronously send batch request * * @method sendBatch - * @param {Array} batch data + * @param {Array} data - array of payload objects * @param {Function} callback */ RequestManager.prototype.sendBatch = function (data, callback) { @@ -179,19 +207,19 @@ RequestManager.prototype.sendBatch = function (data, callback) { * Waits for notifications * * @method addSubscription - * @param {String} id the subscription id - * @param {String} name the subscription name + * @param {Subscription} subscription the subscription * @param {String} type the subscription namespace (eth, personal, etc) * @param {Function} callback the callback to call for incoming notifications */ -RequestManager.prototype.addSubscription = function (id, name, type, callback) { - if(this.provider.on) { - this.subscriptions[id] = { - callback: callback, - type: type, - name: name - }; - +RequestManager.prototype.addSubscription = function (subscription, callback) { + if (this.provider.on) { + this.subscriptions.set( + subscription.id, + { + callback: callback, + subscription: subscription + } + ); } else { throw new Error('The provider doesn\'t support subscriptions: '+ this.provider.constructor.name); } @@ -205,17 +233,24 @@ RequestManager.prototype.addSubscription = function (id, name, type, callback) { * @param {Function} callback fired once the subscription is removed */ RequestManager.prototype.removeSubscription = function (id, callback) { - var _this = this; + if (this.subscriptions.has(id)) { + var type = this.subscriptions.get(id).subscription.options.type; - if(this.subscriptions[id]) { + // remove subscription first to avoid reentry + this.subscriptions.delete(id); + // then, try to actually unsubscribe this.send({ - method: this.subscriptions[id].type + '_unsubscribe', + method: type + '_unsubscribe', params: [id] }, callback); - // remove subscription - delete _this.subscriptions[id]; + return; + } + + if (typeof callback === 'function') { + // call the callback if the subscription was already removed + callback(null); } }; @@ -227,21 +262,45 @@ RequestManager.prototype.removeSubscription = function (id, callback) { RequestManager.prototype.clearSubscriptions = function (keepIsSyncing) { var _this = this; - - if (this.subscriptions) { - // uninstall all subscriptions - Object.keys(this.subscriptions).forEach(function(id){ - if(!keepIsSyncing || _this.subscriptions[id].name !== 'syncing') + // uninstall all subscriptions + if (this.subscriptions.size > 0) { + this.subscriptions.forEach(function (value, id) { + if (!keepIsSyncing || value.name !== 'syncing') _this.removeSubscription(id); }); } - // reset notification callbacks etc. if(this.provider.reset) this.provider.reset(); }; +/** + * Evaluates WS close event + * + * @method _isCleanClose + * + * @param {CloseEvent | boolean} event WS close event or exception flag + * + * @returns {boolean} + */ +RequestManager.prototype._isCleanCloseEvent = function (event) { + return typeof event === 'object' && ([1000].includes(event.code) || event.wasClean === true); +}; + +/** + * Detects Ipc close error. The node.net module emits ('close', isException) + * + * @method _isIpcCloseError + * + * @param {CloseEvent | boolean} event WS close event or exception flag + * + * @returns {boolean} + */ +RequestManager.prototype._isIpcCloseError = function (event) { + return typeof event === 'boolean' && event; +}; + module.exports = { Manager: RequestManager, BatchManager: BatchManager diff --git a/packages/web3-core-subscriptions/src/index.js b/packages/web3-core-subscriptions/src/index.js index e12811af8e5..113f6757fd9 100644 --- a/packages/web3-core-subscriptions/src/index.js +++ b/packages/web3-core-subscriptions/src/index.js @@ -59,7 +59,7 @@ Subscriptions.prototype.buildCall = function() { } var subscription = new Subscription({ - subscription: _this.subscriptions[arguments[0]], + subscription: _this.subscriptions[arguments[0]] || {}, // Subscript might not exist requestManager: _this.requestManager, type: _this.type }); diff --git a/packages/web3-core-subscriptions/src/subscription.js b/packages/web3-core-subscriptions/src/subscription.js index 92d24551b45..dc44cf4c613 100644 --- a/packages/web3-core-subscriptions/src/subscription.js +++ b/packages/web3-core-subscriptions/src/subscription.js @@ -32,7 +32,6 @@ function Subscription(options) { this.id = null; this.callback = _.identity; this.arguments = null; - this._reconnectIntervalId = null; this.options = { subscription: options.subscription, @@ -78,7 +77,11 @@ Subscription.prototype._validateArgs = function (args) { subscription.params = 0; if (args.length !== subscription.params) { - throw errors.InvalidNumberOfParams(args.length, subscription.params + 1, args[0]); + throw errors.InvalidNumberOfParams( + args.length, + subscription.params, + subscription.subscriptionName + ); } }; @@ -174,7 +177,6 @@ Subscription.prototype.unsubscribe = function(callback) { this.options.requestManager.removeSubscription(this.id, callback); this.id = null; this.removeAllListeners(); - clearInterval(this._reconnectIntervalId); }; /** @@ -194,18 +196,28 @@ Subscription.prototype.subscribe = function() { return this; } + // throw error, if provider is not set if(!this.options.requestManager.provider) { - var err1 = new Error('No provider set.'); - this.callback(err1, null, this); - this.emit('error', err1); + setTimeout(function(){ + var err1 = new Error('No provider set.'); + _this.callback(err1, null, _this); + _this.emit('error', err1); + },0); + return this; } // throw error, if provider doesnt support subscriptions if(!this.options.requestManager.provider.on) { - var err2 = new Error('The current provider doesn\'t support subscriptions: '+ this.options.requestManager.provider.constructor.name); - this.callback(err2, null, this); - this.emit('error', err2); + setTimeout(function(){ + var err2 = new Error( + 'The current provider doesn\'t support subscriptions: ' + + _this.options.requestManager.provider.constructor.name + ); + _this.callback(err2, null, _this); + _this.emit('error', err2); + },0); + return this; } @@ -234,8 +246,10 @@ Subscription.prototype.subscribe = function() { // TODO subscribe here? after the past logs? } else { - _this.callback(err, null, _this); - _this.emit('error', err); + setTimeout(function(){ + _this.callback(err, null, _this); + _this.emit('error', err); + },0); } }); } @@ -249,12 +263,12 @@ Subscription.prototype.subscribe = function() { this.options.requestManager.send(payload, function (err, result) { if(!err && result) { _this.id = result; + _this.method = payload.params[0]; _this.emit('connected', result); // call callback on notifications - _this.options.requestManager.addSubscription(_this.id, payload.params[0] , _this.options.type, function(err, result) { - - if (!err) { + _this.options.requestManager.addSubscription(_this, function(error, result) { + if (!error) { if (!_.isArray(result)) { result = [result]; } @@ -272,32 +286,15 @@ Subscription.prototype.subscribe = function() { _this.callback(null, output, _this); }); } else { - // unsubscribe, but keep listeners - _this.options.requestManager.removeSubscription(_this.id); - - // re-subscribe, if connection fails - if(_this.options.requestManager.provider.once) { - _this._reconnectIntervalId = setInterval(function () { - // TODO check if that makes sense! - if (_this.options.requestManager.provider.reconnect) { - _this.options.requestManager.provider.reconnect(); - } - }, 500); - - _this.options.requestManager.provider.once('connect', function () { - clearInterval(_this._reconnectIntervalId); - _this.subscribe(_this.callback); - }); - } - _this.emit('error', err); - - // call the callback, last so that unsubscribe there won't affect the emit above - _this.callback(err, null, _this); + _this.callback(error, false, _this); + _this.emit('error', error); } }); } else { - _this.callback(err, null, _this); - _this.emit('error', err); + setTimeout(function(){ + _this.callback(err, false, _this); + _this.emit('error', err); + },0); } }); @@ -305,4 +302,18 @@ Subscription.prototype.subscribe = function() { return this; }; +/** + * Resubscribe + * + * @method resubscribe + * + * @returns {void} + */ +Subscription.prototype.resubscribe = function () { + this.options.requestManager.removeSubscription(this.id); // unsubscribe + this.id = null; + + this.subscribe(this.callback); +}; + module.exports = Subscription; diff --git a/packages/web3-core/src/index.js b/packages/web3-core/src/index.js index 29ecd36fdea..93ca90e70e8 100644 --- a/packages/web3-core/src/index.js +++ b/packages/web3-core/src/index.js @@ -34,7 +34,6 @@ module.exports = { throw new Error('You need to instantiate using the "new" keyword.'); } - // make property of pkg._provider, which can properly set providers Object.defineProperty(pkg, 'currentProvider', { get: function () { @@ -47,21 +46,18 @@ module.exports = { configurable: true }); - // inherit from web3 umbrella package + // inherit from parent package or create a new RequestManager if (args[0] && args[0]._requestManager) { - pkg._requestManager = new requestManager.Manager(args[0].currentProvider); - - // set requestmanager on package + pkg._requestManager = args[0]._requestManager; } else { - pkg._requestManager = new requestManager.Manager(); - pkg._requestManager.setProvider(args[0], args[1]); + pkg._requestManager = new requestManager.Manager(args[0], args[1]); } // add givenProvider pkg.givenProvider = requestManager.Manager.givenProvider; pkg.providers = requestManager.Manager.providers; - pkg._provider = pkg._requestManager.provider; + pkg._provider = pkg._requestManager.provider; // add SETPROVIDER function (don't overwrite if already existing) if (!pkg.setProvider) { @@ -72,6 +68,11 @@ module.exports = { }; } + pkg.setRequestManager = function(manager) { + pkg._requestManager = manager; + pkg._provider = manager.provider; + }; + // attach batch request creation pkg.BatchRequest = requestManager.BatchManager.bind(null, pkg._requestManager); @@ -83,4 +84,3 @@ module.exports = { pkg.providers = requestManager.Manager.providers; } }; - diff --git a/packages/web3-core/types/index.d.ts b/packages/web3-core/types/index.d.ts index d682ea388bd..fd76ae6fbea 100644 --- a/packages/web3-core/types/index.d.ts +++ b/packages/web3-core/types/index.d.ts @@ -394,8 +394,6 @@ export class IpcProvider extends IpcProviderBase { export class WebsocketProvider extends WebsocketProviderBase { constructor(host: string, options?: WebsocketProviderOptions); - - isConnecting(): boolean; } export interface PastLogsOptions extends LogsOptions { diff --git a/packages/web3-eth-contract/src/index.js b/packages/web3-eth-contract/src/index.js index 9de7a40e8db..1f73d2e5ff1 100644 --- a/packages/web3-eth-contract/src/index.js +++ b/packages/web3-eth-contract/src/index.js @@ -56,22 +56,24 @@ var Contract = function Contract(jsonInterface, address, options) { args = Array.prototype.slice.call(arguments); if(!(this instanceof Contract)) { - throw new Error('Please use the "new" keyword to instantiate a web3.eth.contract() object!'); + throw new Error('Please use the "new" keyword to instantiate a web3.eth.Contract() object!'); } - // sets _requestManager - core.packageInit(this, [this.constructor.currentProvider]); + this.setProvider = function () { + core.packageInit(_this, arguments); - this.clearSubscriptions = this._requestManager.clearSubscriptions; + _this.clearSubscriptions = _this._requestManager.clearSubscriptions; + }; + // sets _requestmanager + core.packageInit(this, [this.constructor]); + this.clearSubscriptions = this._requestManager.clearSubscriptions; if(!jsonInterface || !(Array.isArray(jsonInterface))) { throw new Error('You must provide the json interface of the contract when instantiating a contract object.'); } - - // create the options object this.options = {}; @@ -298,6 +300,17 @@ var Contract = function Contract(jsonInterface, address, options) { }; +/** + * Sets the new provider, creates a new requestManager, registers the "data" listener on the provider and sets the + * accounts module for the Contract class. + * + * @method setProvider + * + * @param {string|provider} provider + * @param {Accounts} accounts + * + * @returns void + */ Contract.setProvider = function(provider, accounts) { // Contract.currentProvider = provider; core.packageInit(this, [provider]); @@ -690,9 +703,11 @@ Contract.prototype.once = function(event, options, callback) { * Adds event listeners and creates a subscription. * * @method _on + * * @param {String} event * @param {Object} options * @param {Function} callback + * * @return {Object} the event subscription */ Contract.prototype._on = function(){ @@ -700,8 +715,8 @@ Contract.prototype._on = function(){ // prevent the event "newListener" and "removeListener" from being overwritten - this._checkListener('newListener', subOptions.event.name, subOptions.callback); - this._checkListener('removeListener', subOptions.event.name, subOptions.callback); + this._checkListener('newListener', subOptions.event.name); + this._checkListener('removeListener', subOptions.event.name); // TODO check if listener already exists? and reuse subscription if options are the same. @@ -727,6 +742,7 @@ Contract.prototype._on = function(){ type: 'eth', requestManager: this._requestManager }); + subscription.subscribe('logs', subOptions.params, subOptions.callback || function () {}); return subscription; diff --git a/packages/web3-eth-personal/src/index.js b/packages/web3-eth-personal/src/index.js index 54f12ab35ef..ab27b1ef4aa 100644 --- a/packages/web3-eth-personal/src/index.js +++ b/packages/web3-eth-personal/src/index.js @@ -36,7 +36,7 @@ var Personal = function Personal() { // sets _requestmanager core.packageInit(this, arguments); - this.net = new Net(this.currentProvider); + this.net = new Net(this); var defaultAccount = null; var defaultBlock = 'latest'; diff --git a/packages/web3-eth/src/index.js b/packages/web3-eth/src/index.js index 9053cac35cb..630ce47a618 100644 --- a/packages/web3-eth/src/index.js +++ b/packages/web3-eth/src/index.js @@ -68,14 +68,26 @@ var Eth = function Eth() { // sets _requestmanager core.packageInit(this, arguments); + // overwrite package setRequestManager + var setRequestManager = this.setRequestManager; + this.setRequestManager = function (manager) { + setRequestManager(manager); + + _this.net.setRequestManager(manager); + _this.personal.setRequestManager(manager); + _this.accounts.setRequestManager(manager); + _this.Contract._requestManager = _this._requestManager; + _this.Contract.currentProvider = _this._provider; + + return true; + }; + // overwrite setProvider var setProvider = this.setProvider; this.setProvider = function () { setProvider.apply(_this, arguments); - _this.net.setProvider.apply(_this, arguments); - _this.personal.setProvider.apply(_this, arguments); - _this.accounts.setProvider.apply(_this, arguments); - _this.Contract.setProvider(_this.currentProvider, _this.accounts); + + _this.setRequestManager(_this._requestManager); // Set detectedAddress/lastSyncCheck back to null because the provider could be connected to a different chain now _this.ens._detectedAddress = null; @@ -256,15 +268,15 @@ var Eth = function Eth() { this.clearSubscriptions = _this._requestManager.clearSubscriptions; // add net - this.net = new Net(this.currentProvider); + this.net = new Net(this); // add chain detection this.net.getNetworkType = getNetworkType.bind(this); // add accounts - this.accounts = new Accounts(this.currentProvider); + this.accounts = new Accounts(this); // add personal - this.personal = new Personal(this.currentProvider); + this.personal = new Personal(this); this.personal.defaultAccount = this.defaultAccount; // create a proxy Contract type for this instance, as a Contract's provider @@ -284,7 +296,7 @@ var Eth = function Eth() { var setProvider = self.setProvider; self.setProvider = function() { setProvider.apply(self, arguments); - core.packageInit(_this, [self.currentProvider]); + core.packageInit(_this, [self]); }; }; @@ -305,7 +317,9 @@ var Eth = function Eth() { this.Contract.transactionConfirmationBlocks = this.transactionConfirmationBlocks; this.Contract.transactionPollingTimeout = this.transactionPollingTimeout; this.Contract.handleRevert = this.handleRevert; - this.Contract.setProvider(this.currentProvider, this.accounts); + this.Contract._requestManager = this._requestManager; + this.Contract._ethAccounts = this.accounts; + this.Contract.currentProvider = this._requestManager.provider; // add IBAN this.Iban = Iban; @@ -611,7 +625,7 @@ var Eth = function Eth() { methods.forEach(function(method) { method.attachToObject(_this); - method.setRequestManager(_this._requestManager, _this.accounts); // second param means is eth.accounts (necessary for wallet signing) + method.setRequestManager(_this._requestManager, _this.accounts); // second param is the eth.accounts module (necessary for signing transactions locally) method.defaultBlock = _this.defaultBlock; method.defaultAccount = _this.defaultAccount; method.transactionBlockTimeout = _this.transactionBlockTimeout; @@ -622,6 +636,7 @@ var Eth = function Eth() { }; +// Adds the static givenProvider and providers property to the Eth module core.addProviders(Eth); diff --git a/packages/web3-providers-ws/README.md b/packages/web3-providers-ws/README.md index 2dac56e5068..a0f2af6f336 100644 --- a/packages/web3-providers-ws/README.md +++ b/packages/web3-providers-ws/README.md @@ -34,10 +34,33 @@ This will expose the `Web3WsProvider` object on the window object. var Web3WsProvider = require('web3-providers-ws'); var options = { - timeout: 30000, - headers: { authorization: 'Basic username:password' } -}; // set a custom timeout at 30 seconds, and credentials (you can also add the credentials to the URL: ws://username:password@localhost:8546) + timeout: 30000, // ms + + // Useful for credentialed urls, e.g: ws://username:password@localhost:8546 + headers: { + authorization: 'Basic username:password' + }, + + // Useful if requests are large + clientConfig: { + maxReceivedFrameSize: 100000000, // bytes - default: 1MiB + maxReceivedMessageSize: 100000000, // bytes - default: 8MiB + }, + + // Enable auto reconnection + reconnect: { + auto: true, + delay: 5000, // ms + maxAttempts: 5, + onTimeout: false + } +}; + var ws = new Web3WsProvider('ws://localhost:8546', options); + +(Additional client config options can be found [here][1]) + +[1]: https://github.com/web3-js/WebSocket-Node/blob/polyfill/globalThis/docs/WebSocketClient.md ``` ## Types diff --git a/packages/web3-providers-ws/package-lock.json b/packages/web3-providers-ws/package-lock.json index 0d66cf20758..23a80348e2d 100644 --- a/packages/web3-providers-ws/package-lock.json +++ b/packages/web3-providers-ws/package-lock.json @@ -159,7 +159,7 @@ "integrity": "sha512-ph4GXLw3HYzlQMJOFcpCqWHuL3MxJ/344OR7wn0wlQGchQGTIVNsSUl8iKEMatpy2geNMysgA9fQa6xVhHOkTQ==", "dev": true, "requires": { - "definitelytyped-header-parser": "github:Microsoft/definitelytyped-header-parser#production", + "definitelytyped-header-parser": "github:Microsoft/definitelytyped-header-parser#d957ad0bb2f4ecb60ac04f734e0b38fbc8e70b8a", "fs-extra": "^6.0.1", "strip-json-comments": "^2.0.1", "tslint": "^5.12.0", @@ -224,6 +224,11 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "eventemitter3": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", + "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==" + }, "ext": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", diff --git a/packages/web3-providers-ws/package.json b/packages/web3-providers-ws/package.json index fd909f4c51b..4a279b8991a 100644 --- a/packages/web3-providers-ws/package.json +++ b/packages/web3-providers-ws/package.json @@ -14,6 +14,7 @@ "main": "src/index.js", "dependencies": { "@web3-js/websocket": "^1.0.29", + "eventemitter3": "^4.0.0", "underscore": "1.9.1", "web3-core-helpers": "1.2.6" }, diff --git a/packages/web3-providers-ws/src/helpers.js b/packages/web3-providers-ws/src/helpers.js new file mode 100644 index 00000000000..eff553bd9a4 --- /dev/null +++ b/packages/web3-providers-ws/src/helpers.js @@ -0,0 +1,30 @@ +var isNode = Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]'; + +var _btoa = null; +var helpers = null; +if (isNode) { + _btoa = function(str) { + return Buffer.from(str).toString('base64'); + }; + var url = require('url'); + if (url.URL) { + // Use the new Node 6+ API for parsing URLs that supports username/password + var newURL = url.URL; + helpers = function(url) { + return new newURL(url); + }; + } else { + // Web3 supports Node.js 5, so fall back to the legacy URL API if necessary + helpers = require('url').parse; + } +} else { + _btoa = btoa; + helpers = function(url) { + return new URL(url); + }; +} + +module.exports = { + parseURL: helpers, + btoa: _btoa +}; diff --git a/packages/web3-providers-ws/src/index.js b/packages/web3-providers-ws/src/index.js index 15cc1ab6b4e..006a9224a57 100644 --- a/packages/web3-providers-ws/src/index.js +++ b/packages/web3-providers-ws/src/index.js @@ -14,194 +14,266 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -/** @file WebsocketProvider.js - * @authors: - * Fabian Vogelsteller - * @date 2017 +/** + * @file WebsocketProvider.js + * @authors: Samuel Furter , Fabian Vogelsteller + * @date 2019 */ -"use strict"; +'use strict'; -var _ = require('underscore'); +var EventEmitter = require('eventemitter3'); +var helpers = require('./helpers.js'); var errors = require('web3-core-helpers').errors; var Ws = require('@web3-js/websocket').w3cwebsocket; -var isNode = Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]'; - -var _btoa = null; -var parseURL = null; -if (isNode) { - _btoa = function(str) { - return Buffer.from(str).toString('base64'); - }; - var url = require('url'); - if (url.URL) { - // Use the new Node 6+ API for parsing URLs that supports username/password - var newURL = url.URL; - parseURL = function(url) { - return new newURL(url); - }; - } - else { - // Web3 supports Node.js 5, so fall back to the legacy URL API if necessary - parseURL = require('url').parse; - } -} else { - _btoa = btoa; - parseURL = function(url) { - return new URL(url); - }; -} -// Default connection ws://localhost:8546 - - - - -var WebsocketProvider = function WebsocketProvider(url, options) { - if (!Ws) { - throw new Error('websocket is not available'); - } - - var _this = this; - this.responseCallbacks = {}; - this.notificationCallbacks = []; +/** + * @param {string} url + * @param {Object} options + * + * @constructor + */ +var WebsocketProvider = function WebsocketProvider(url, options) { + EventEmitter.call(this); options = options || {}; - this._customTimeout = options.timeout; + this.url = url; + this._customTimeout = options.timeout || 1000 * 15; + this.headers = options.headers || {}; + this.protocol = options.protocol || undefined; + this.reconnectOptions = Object.assign({ + auto: false, + delay: 5000, + maxAttempts: false, + onTimeout: false + }, + options.reconnect + ); + this.clientConfig = options.clientConfig || undefined; // Allow a custom client configuration + this.requestOptions = options.requestOptions || undefined; // Allow a custom request options (https://github.com/theturtle32/WebSocket-Node/blob/master/docs/WebSocketClient.md#connectrequesturl-requestedprotocols-origin-headers-requestoptions) + + this.DATA = 'data'; + this.CLOSE = 'close'; + this.ERROR = 'error'; + this.CONNECT = 'connect'; + this.RECONNECT = 'reconnect'; + + this.connection = null; + this.requestQueue = new Map(); + this.responseQueue = new Map(); + this.reconnectAttempts = 0; + this.reconnecting = false; // The w3cwebsocket implementation does not support Basic Auth // username/password in the URL. So generate the basic auth header, and // pass through with any additional headers supplied in constructor - var parsedURL = parseURL(url); - var headers = options.headers || {}; - var protocol = options.protocol || undefined; + var parsedURL = helpers.parseURL(url); if (parsedURL.username && parsedURL.password) { - headers.authorization = 'Basic ' + _btoa(parsedURL.username + ':' + parsedURL.password); + this.headers.authorization = 'Basic ' + helpers.btoa(parsedURL.username + ':' + parsedURL.password); } - // Allow a custom client configuration - var clientConfig = options.clientConfig || undefined; - - // Allow a custom request options - // https://github.com/theturtle32/WebSocket-Node/blob/master/docs/WebSocketClient.md#connectrequesturl-requestedprotocols-origin-headers-requestoptions - var requestOptions = options.requestOptions || undefined; - // When all node core implementations that do not have the // WHATWG compatible URL parser go out of service this line can be removed. if (parsedURL.auth) { - headers.authorization = 'Basic ' + _btoa(parsedURL.auth); + this.headers.authorization = 'Basic ' + helpers.btoa(parsedURL.auth); } - this.connection = new Ws(url, protocol, undefined, headers, requestOptions, clientConfig); - - this.addDefaultEvents(); - - - // LISTEN FOR CONNECTION RESPONSES - this.connection.onmessage = function(e) { - /*jshint maxcomplexity: 6 */ - var data = (typeof e.data === 'string') ? e.data : ''; - - _this._parseResponse(data).forEach(function(result){ - - var id = null; - - // get the id which matches the returned id - if(_.isArray(result)) { - result.forEach(function(load){ - if(_this.responseCallbacks[load.id]) - id = load.id; - }); - } else { - id = result.id; - } - - // notification - if(!id && result && result.method && result.method.indexOf('_subscription') !== -1) { - _this.notificationCallbacks.forEach(function(callback){ - if(_.isFunction(callback)) - callback(result); - }); - - // fire the callback - } else if(_this.responseCallbacks[id]) { - _this.responseCallbacks[id](null, result); - delete _this.responseCallbacks[id]; - } - }); - }; // make property `connected` which will return the current connection status Object.defineProperty(this, 'connected', { - get: function () { - return this.connection && this.connection.readyState === this.connection.OPEN; - }, - enumerable: true, - }); + get: function () { + return this.connection && this.connection.readyState === this.connection.OPEN; + }, + enumerable: true + }); + + this.connect(); +}; + +// Inherit from EventEmitter +WebsocketProvider.prototype = Object.create(EventEmitter.prototype); +WebsocketProvider.prototype.constructor = WebsocketProvider; + +/** + * Connects to the configured node + * + * @method connect + * + * @returns {void} + */ +WebsocketProvider.prototype.connect = function () { + this.connection = new Ws(this.url, this.protocol, undefined, this.headers, this.requestOptions, this.clientConfig); + this._addSocketListeners(); +}; + +/** + * Listener for the `data` event of the underlying WebSocket object + * + * @method _onMessage + * + * @returns {void} + */ +WebsocketProvider.prototype._onMessage = function (e) { + var _this = this; + + this._parseResponse((typeof e.data === 'string') ? e.data : '').forEach(function (result) { + if (result.method && result.method.indexOf('_subscription') !== -1) { + _this.emit(_this.DATA, result); + + return; + } + + var id = result.id; + + // get the id which matches the returned id + if (Array.isArray(result)) { + id = result[0].id; + } + + if (_this.responseQueue.has(id)) { + _this.responseQueue.get(id).callback(false, result); + _this.responseQueue.delete(id); + } + }); }; /** - Will add the error and end event to timeout existing calls + * Listener for the `open` event of the underlying WebSocket object + * + * @method _onConnect + * + * @returns {void} + */ +WebsocketProvider.prototype._onConnect = function () { + this.emit(this.CONNECT); + this.reconnectAttempts = 0; + this.reconnecting = false; + + if (this.requestQueue.size > 0) { + var _this = this; + + this.requestQueue.forEach(function (request, key) { + _this.send(request.payload, request.callback); + _this.requestQueue.delete(key); + }); + } +}; - @method addDefaultEvents +/** + * Listener for the `close` event of the underlying WebSocket object + * + * @method _onClose + * + * @returns {void} */ -WebsocketProvider.prototype.addDefaultEvents = function(){ +WebsocketProvider.prototype._onClose = function (event) { var _this = this; - this.connection.onerror = function(){ - _this._timeout(); - }; + if (this.reconnectOptions.auto && (![1000, 1001].includes(event.code) || event.wasClean === false)) { + this.reconnect(); + + return; + } + + this.emit(this.CLOSE, event); + + if (this.requestQueue.size > 0) { + this.requestQueue.forEach(function (request, key) { + request.callback(errors.ConnectionNotOpenError(event)); + _this.requestQueue.delete(key); + }); + } - this.connection.onclose = function(){ - _this._timeout(); + if (this.responseQueue.size > 0) { + this.responseQueue.forEach(function (request, key) { + request.callback(errors.InvalidConnection('on WS', event)); + _this.responseQueue.delete(key); + }); + } - // reset all requests and callbacks - _this.reset(); - }; + this._removeSocketListeners(); + this.removeAllListeners(); +}; - // this.connection.on('timeout', function(){ - // _this._timeout(); - // }); +/** + * Will add the required socket listeners + * + * @method _addSocketListeners + * + * @returns {void} + */ +WebsocketProvider.prototype._addSocketListeners = function () { + this.connection.addEventListener('message', this._onMessage.bind(this)); + this.connection.addEventListener('open', this._onConnect.bind(this)); + this.connection.addEventListener('close', this._onClose.bind(this)); }; /** - Will parse the response and make an array out of it. + * Will remove all socket listeners + * + * @method _removeSocketListeners + * + * @returns {void} + */ +WebsocketProvider.prototype._removeSocketListeners = function () { + this.connection.removeEventListener('message', this._onMessage); + this.connection.removeEventListener('open', this._onConnect); + this.connection.removeEventListener('close', this._onClose); +}; - @method _parseResponse - @param {String} data +/** + * Will parse the response and make an array out of it. + * + * @method _parseResponse + * + * @param {String} data + * + * @returns {Array} */ -WebsocketProvider.prototype._parseResponse = function(data) { +WebsocketProvider.prototype._parseResponse = function (data) { var _this = this, returnValues = []; // DE-CHUNKER var dechunkedData = data - .replace(/\}[\n\r]?\{/g,'}|--|{') // }{ - .replace(/\}\][\n\r]?\[\{/g,'}]|--|[{') // }][{ - .replace(/\}[\n\r]?\[\{/g,'}|--|[{') // }[{ - .replace(/\}\][\n\r]?\{/g,'}]|--|{') // }]{ + .replace(/\}[\n\r]?\{/g, '}|--|{') // }{ + .replace(/\}\][\n\r]?\[\{/g, '}]|--|[{') // }][{ + .replace(/\}[\n\r]?\[\{/g, '}|--|[{') // }[{ + .replace(/\}\][\n\r]?\{/g, '}]|--|{') // }]{ .split('|--|'); - dechunkedData.forEach(function(data){ + dechunkedData.forEach(function (data) { // prepend the last chunk - if(_this.lastChunk) + if (_this.lastChunk) data = _this.lastChunk + data; var result = null; try { result = JSON.parse(data); - - } catch(e) { + } catch (e) { _this.lastChunk = data; // start timeout to cancel all requests clearTimeout(_this.lastChunkTimeout); - _this.lastChunkTimeout = setTimeout(function(){ - _this._timeout(); - throw errors.InvalidResponse(data); - }, 1000 * 15); + _this.lastChunkTimeout = setTimeout(function () { + if (_this.reconnectOptions.auto && _this.reconnectOptions.onTimeout) { + _this.reconnect(); + + return; + } + + + _this.emit(_this.ERROR, errors.ConnectionTimeout(_this._customTimeout)); + + if (_this.requestQueue.size > 0) { + _this.requestQueue.forEach(function (request, key) { + request.callback(errors.ConnectionTimeout(_this._customTimeout)); + _this.requestQueue.delete(key); + }); + } + }, _this._customTimeout); return; } @@ -210,208 +282,141 @@ WebsocketProvider.prototype._parseResponse = function(data) { clearTimeout(_this.lastChunkTimeout); _this.lastChunk = null; - if(result) + if (result) returnValues.push(result); }); return returnValues; }; - /** - Adds a callback to the responseCallbacks object, - which will be called if a response matching the response Id will arrive. - - @method _addResponseCallback + * Does check if the provider is connecting and will add it to the queue or will send it directly + * + * @method send + * + * @param {Object} payload + * @param {Function} callback + * + * @returns {void} */ -WebsocketProvider.prototype._addResponseCallback = function(payload, callback) { - var id = payload.id || payload[0].id; - var method = payload.method || payload[0].method; - - this.responseCallbacks[id] = callback; - this.responseCallbacks[id].method = method; - +WebsocketProvider.prototype.send = function (payload, callback) { var _this = this; + var id = payload.id; + var request = {payload: payload, callback: callback}; - // schedule triggering the error response if a custom timeout is set - if (this._customTimeout) { - setTimeout(function () { - if (_this.responseCallbacks[id]) { - _this.responseCallbacks[id](errors.ConnectionTimeout(_this._customTimeout)); - delete _this.responseCallbacks[id]; - } - }, this._customTimeout); + if (Array.isArray(payload)) { + id = payload[0].id; } -}; -/** - Timeout all requests when the end/error event is fired + if (this.connection.readyState === this.connection.CONNECTING || this.reconnecting) { + this.requestQueue.set(id, request); - @method _timeout - */ -WebsocketProvider.prototype._timeout = function() { - for(var key in this.responseCallbacks) { - if(this.responseCallbacks.hasOwnProperty(key)){ - this.responseCallbacks[key](errors.InvalidConnection('on WS')); - delete this.responseCallbacks[key]; - } + return; } -}; + if (this.connection.readyState !== this.connection.OPEN) { + this.requestQueue.delete(id); -WebsocketProvider.prototype.send = function (payload, callback) { - var _this = this; + this.emit(this.ERROR, errors.ConnectionNotOpenError()); + request.callback(errors.ConnectionNotOpenError()); - if (this.connection.readyState === this.connection.CONNECTING) { - setTimeout(function () { - _this.send(payload, callback); - }, 10); return; } - // try reconnect, when connection is gone - // if(!this.connection.writable) - // this.connection.connect({url: this.url}); - if (this.connection.readyState !== this.connection.OPEN) { - console.error('connection not open on send()'); - if (typeof this.connection.onerror === 'function') { - this.connection.onerror(new Error('connection not open')); - } else { - console.error('no error callback'); - } - callback(new Error('connection not open')); - return; - } + this.responseQueue.set(id, request); + this.requestQueue.delete(id); - this.connection.send(JSON.stringify(payload)); - this._addResponseCallback(payload, callback); + try { + this.connection.send(JSON.stringify(request.payload)); + } catch (error) { + request.callback(error); + _this.responseQueue.delete(id); + } }; /** - Subscribes to provider events.provider - - @method on - @param {String} type 'notifcation', 'connect', 'error', 'end' or 'data' - @param {Function} callback the callback to call + * Resets the providers, clears all callbacks + * + * @method reset + * + * @returns {void} */ -WebsocketProvider.prototype.on = function (type, callback) { - - if(typeof callback !== 'function') - throw new Error('The second parameter callback must be a function.'); - - switch(type){ - case 'data': - this.notificationCallbacks.push(callback); - break; - - case 'connect': - this.connection.onopen = callback; - break; - - case 'end': - this.connection.onclose = callback; - break; +WebsocketProvider.prototype.reset = function () { + this.responseQueue.clear(); + this.requestQueue.clear(); - case 'error': - this.connection.onerror = callback; - break; + this.removeAllListeners(); - // default: - // this.connection.on(type, callback); - // break; - } + this._removeSocketListeners(); + this._addSocketListeners(); }; -// TODO add once - /** - Removes event listener - - @method removeListener - @param {String} type 'notifcation', 'connect', 'error', 'end' or 'data' - @param {Function} callback the callback to call + * Closes the current connection with the given code and reason arguments + * + * @method disconnect + * + * @param {number} code + * @param {string} reason + * + * @returns {void} */ -WebsocketProvider.prototype.removeListener = function (type, callback) { - var _this = this; - - switch(type){ - case 'data': - this.notificationCallbacks.forEach(function(cb, index){ - if(cb === callback) - _this.notificationCallbacks.splice(index, 1); - }); - break; - - // TODO remvoving connect missing - - // default: - // this.connection.removeListener(type, callback); - // break; - } +WebsocketProvider.prototype.disconnect = function (code, reason) { + this._removeSocketListeners(); + this.connection.close(code || 1000, reason); }; /** - Removes all event listeners - - @method removeAllListeners - @param {String} type 'notifcation', 'connect', 'error', 'end' or 'data' + * Returns the desired boolean. + * + * @method supportsSubscriptions + * + * @returns {boolean} */ -WebsocketProvider.prototype.removeAllListeners = function (type) { - switch(type){ - case 'data': - this.notificationCallbacks = []; - break; - - // TODO remvoving connect properly missing - - case 'connect': - this.connection.onopen = null; - break; - - case 'end': - this.connection.onclose = null; - break; - - case 'error': - this.connection.onerror = null; - break; - - default: - // this.connection.removeAllListeners(type); - break; - } +WebsocketProvider.prototype.supportsSubscriptions = function () { + return true; }; /** - Resets the providers, clears all callbacks - - @method reset + * Removes the listeners and reconnects to the socket. + * + * @method reconnect + * + * @returns {void} */ -WebsocketProvider.prototype.reset = function () { - this._timeout(); - this.notificationCallbacks = []; +WebsocketProvider.prototype.reconnect = function () { + var _this = this; + this.reconnecting = true; - // this.connection.removeAllListeners('error'); - // this.connection.removeAllListeners('end'); - // this.connection.removeAllListeners('timeout'); + if (this.responseQueue.size > 0) { + this.responseQueue.forEach(function (request, key) { + request.callback(errors.PendingRequestsOnReconnectingError()); + _this.responseQueue.delete(key); + }); + } - this.addDefaultEvents(); -}; + if ( + !this.reconnectOptions.maxAttempts || + this.reconnectAttempts < this.reconnectOptions.maxAttempts + ) { + setTimeout(function () { + _this.reconnectAttempts++; + _this._removeSocketListeners(); + _this.emit(_this.RECONNECT, _this.reconnectAttempts); + _this.connect(); + }, this.reconnectOptions.delay); -WebsocketProvider.prototype.disconnect = function () { - if (this.connection) { - this.connection.close(); + return; } -}; -/** - * Returns the desired boolean. - * - * @method supportsSubscriptions - * @returns {boolean} - */ -WebsocketProvider.prototype.supportsSubscriptions = function () { - return true; + this.emit(this.ERROR, errors.MaxAttemptsReachedOnReconnectingError()); + + if (this.requestQueue.size > 0) { + this.requestQueue.forEach(function (request, key) { + request.callback(errors.MaxAttemptsReachedOnReconnectingError()); + _this.requestQueue.delete(key); + }); + } }; module.exports = WebsocketProvider; diff --git a/packages/web3-providers-ws/types/tests/web3-provider-ws-tests.ts b/packages/web3-providers-ws/types/tests/web3-provider-ws-tests.ts index 6ab7b48f6e6..3d54e0f9473 100644 --- a/packages/web3-providers-ws/types/tests/web3-provider-ws-tests.ts +++ b/packages/web3-providers-ws/types/tests/web3-provider-ws-tests.ts @@ -32,20 +32,20 @@ const options: WebsocketProviderOptions = { const wsProvider = new WebsocketProvider('ws://localhost:8545', options); -// $ExpectType boolean -wsProvider.isConnecting(); - // $ExpectType boolean wsProvider.connected; // $ExpectType void wsProvider.disconnect(100, 'reason'); -// $ExpectType any -wsProvider.responseCallbacks; +// $ExpectType void +wsProvider.reconnect(); -// $ExpectType any -wsProvider.notificationCallbacks; +// $ExpectType Map +wsProvider.requestQueue; + +// $ExpectType Map +wsProvider.responseQueue; // $ExpectType any wsProvider.connection; @@ -53,9 +53,6 @@ wsProvider.connection; // $ExpectType boolean wsProvider.connected; -// $ExpectType void -wsProvider.addDefaultEvents(); - // $ExpectType boolean wsProvider.supportsSubscriptions(); diff --git a/packages/web3-shh/src/index.js b/packages/web3-shh/src/index.js index 740e62e764a..dc7e4d6fdab 100644 --- a/packages/web3-shh/src/index.js +++ b/packages/web3-shh/src/index.js @@ -35,14 +35,25 @@ var Shh = function Shh() { // sets _requestmanager core.packageInit(this, arguments); + // overwrite package setRequestManager + var setRequestManager = this.setRequestManager; + this.setRequestManager = function (manager) { + setRequestManager(manager); + + _this.net.setRequestManager(manager); + + return true; + }; + // overwrite setProvider var setProvider = this.setProvider; this.setProvider = function () { setProvider.apply(_this, arguments); - _this.net.setProvider.apply(_this, arguments); + + _this.setRequestManager(_this._requestManager); }; - this.net = new Net(this.currentProvider); + this.net = new Net(this); [ new Subscriptions({ diff --git a/packages/web3/src/index.js b/packages/web3/src/index.js index 789c1785ac9..2967dd202f5 100644 --- a/packages/web3/src/index.js +++ b/packages/web3/src/index.js @@ -55,9 +55,9 @@ var Web3 = function Web3() { this.setProvider = function (provider, net) { setProvider.apply(_this, arguments); - this.eth.setProvider(provider, net); - this.shh.setProvider(provider, net); - this.bzz.setProvider(provider); + _this.eth.setRequestManager(_this._requestManager); + _this.shh.setRequestManager(_this._requestManager); + _this.bzz.setProvider(provider); return true; }; diff --git a/test/contract.js b/test/contract.js index 7c2d43ec53f..ef3aaafa755 100644 --- a/test/contract.js +++ b/test/contract.js @@ -1178,7 +1178,7 @@ var runTests = function(contractFactory) { assert.equal(result.returnValues.amount, 1); assert.equal(result.returnValues.t1, 1); assert.equal(result.returnValues.t2, 8); - assert.deepEqual(sub.options.requestManager.subscriptions, {}); + assert.deepEqual(sub.options.requestManager.subscriptions, new Map()); assert.equal(count, 1); count++; @@ -1260,7 +1260,7 @@ var runTests = function(contractFactory) { assert.equal(result.returnValues.amount, 1); assert.equal(result.returnValues.t1, 1); assert.equal(result.returnValues.t2, 8); - assert.deepEqual(sub.options.requestManager.subscriptions, {}); + assert.deepEqual(sub.options.requestManager.subscriptions, new Map()); assert.equal(count, 1); count++; @@ -3123,6 +3123,31 @@ describe('typical usage', function() { assert.deepEqual(eth.currentProvider, provider2); }); + it('should update contract instance provider when calling setProvider on itself', function () { + var provider1 = new FakeIpcProvider(); + var provider2 = new FakeHttpProvider(); + + var eth = new Eth(provider1); + var contract = new eth.Contract(abi, address); + assert.deepEqual(contract.currentProvider, provider1); + + contract.setProvider(provider2); + assert.deepEqual(contract.currentProvider, provider2); + }); + + it('errors when invoked without the "new" operator', function () { + try { + var provider = new FakeHttpProvider(); + var eth = new Eth(provider); + + eth.Contract(abi, address); + + assert.fail(); + } catch(err) { + assert(err.message.includes('the "new" keyword')); + } + }); + it('should deploy a contract, sign transaction, and return contract instance', function (done) { var provider = new FakeIpcProvider(); var eth = new Eth(provider); diff --git a/test/e2e.contract.events.js b/test/e2e.contract.events.js index c2993226ad9..15e87acd604 100644 --- a/test/e2e.contract.events.js +++ b/test/e2e.contract.events.js @@ -20,7 +20,7 @@ describe('contract.events [ @E2E ]', function() { gas: 4000000 }; - before(async function(){ + beforeEach(async function(){ var port = utils.getWebsocketPort(); web3 = new Web3('ws://localhost:' + port); @@ -73,6 +73,66 @@ describe('contract.events [ @E2E ]', function() { }); }); + it('should not hear the error handler when connection.closed() called', function(){ + this.timeout(15000); + + let failed = false; + + return new Promise(async (resolve, reject) => { + instance + .events + .BasicEvent({ + fromBlock: 0, + toBlock: 'latest' + }) + .on('error', function(err) { + failed = true; + this.removeAllListeners(); + reject(new Error('err listener should not hear connection.close')); + }); + + await instance + .methods + .firesEvent(accounts[0], 1) + .send({from: accounts[0]}); + + web3.currentProvider.connection.close(); + + // Resolve only if we haven't already rejected + setTimeout(() => { if(!failed) resolve() }, 2500) + }); + }); + + it('should not hear the error handler when provider.disconnect() called', function(){ + this.timeout(15000); + + let failed = false; + + return new Promise(async (resolve, reject) => { + instance + .events + .BasicEvent({ + fromBlock: 0, + toBlock: 'latest' + }) + .on('error', function(err) { + failed = true; + this.removeAllListeners(); + reject(new Error('err listener should not hear provider.disconnect')); + }); + + await instance + .methods + .firesEvent(accounts[0], 1) + .send({from: accounts[0]}); + + web3.currentProvider.disconnect(); + + // Resolve only if we haven't already rejected + setTimeout(() => { if(!failed) resolve() }, 2500) + }); + }); + it('hears events when subscribed to "logs" (emitter)', function(){ return new Promise(async function(resolve, reject){ diff --git a/test/eth.subscribe.ganache.js b/test/eth.subscribe.ganache.js new file mode 100644 index 00000000000..d3d638843cc --- /dev/null +++ b/test/eth.subscribe.ganache.js @@ -0,0 +1,301 @@ +const assert = require('assert'); +const ganache = require('ganache-cli'); +const pify = require('pify'); +const { getWeb3, waitSeconds } = require('./helpers/test.utils'); + +describe('subscription connect/reconnect', function () { + let server; + let web3; + let accounts; + let subscription; + const port = 8545; + const Web3 = getWeb3(); + + beforeEach(async function () { + server = ganache.server({port: port, blockTime: 1}); + await pify(server.listen)(port); + web3 = new Web3('ws://localhost:' + port); + accounts = await web3.eth.getAccounts(); + }); + + afterEach(async function () { + // Might already be closed.. + try { + await pify(server.close)(); + } catch (err) { + } + }); + + it('subscribes (baseline)', function (done) { + web3.eth + .subscribe('newBlockHeaders') + .once('data', function (result) { + assert(result.parentHash); + done(); + }); + }); + + it('subscribes with a callback', function (done) { + subscription = web3.eth + .subscribe('newBlockHeaders', function (err, result) { + assert(result.parentHash); + subscription.unsubscribe(); // Stop listening.. + done(); + }); + }); + + it('subscription emits a connected event', function (done) { + subscription = web3.eth + .subscribe('newBlockHeaders') + .on('connected', function (result) { + assert(result); // First subscription + subscription.unsubscribe(); // Stop listening.. + done(); + }); + }); + + it('resubscribes to an existing subscription', function (done) { + this.timeout(5000); + + let stage = 0; + + subscription = web3.eth.subscribe('newBlockHeaders'); + + subscription.on('data', function (result) { + if (stage === 0) { + subscription.resubscribe(); + stage = 1; + return; + } + + assert(result.parentHash); + subscription.unsubscribe(); // Stop listening.. + done(); + }); + }); + + it('resubscribes after being unsubscribed', function (done) { + this.timeout(5000); + + let stage = 0; + + subscription = web3.eth + .subscribe('newBlockHeaders') + .on('data', function (result) { + assert(result.parentHash); + subscription.unsubscribe(); + stage = 1; + }); + + // Resubscribe from outside + let interval = setInterval(async function () { + if (stage === 1) { + clearInterval(interval); + subscription.resubscribe(); + subscription.on('data', function (result) { + assert(result.parentHash); + subscription.unsubscribe(); // Stop listening.. + done(); + }); + } + }, 500); + }); + + // The ganache unit tests are erroring under similar conditions - + it('does not error when client closes after disconnect', async function(){ + this.timeout(7000); + + return new Promise(async function(resolve, reject) { + web3.eth + .subscribe('newBlockHeaders') + .once("error", function (err) { + reject(new Error('Should not hear an error ')); + }); + + // Let a couple blocks mine.. + await waitSeconds(2) + web3.currentProvider.disconnect(); + + // This delay seems to be required (on Travis). + await waitSeconds(1); + + await pify(server.close)(); + + await waitSeconds(1) + resolve(); + }); + }); + + // Verify subscription cleanup on setProvider + it('does not hear old subscriptions after setting a new provider', async function(){ + this.timeout(7000); + let counter = 0; + + return new Promise(async function(resolve, reject) { + web3.eth + .subscribe('newBlockHeaders') + .on("data", function (_) { + counter++; + }); + + // Let a couple blocks mine.. + await waitSeconds(2) + assert(counter >= 1); + + // Connect to a different client; + const newServer = ganache.server({port: 8777, blockTime: 1}); + await pify(newServer.listen)(8777); + + const finalCount = counter; + web3.setProvider(new Web3.providers.WebsocketProvider('ws://localhost:8777')); + + await waitSeconds(2); + assert.equal(counter, finalCount); + await pify(newServer.close)(); + resolve(); + }); + }) + + it('allows a subscription which does not exist', function () { + web3.eth.subscribe('subscription-does-not-exists'); + }); + + it('errors when zero params subscrip. is called with the wrong arguments', function () { + try { + web3.eth.subscribe('newBlockHeaders', 5); + assert.fail(); + } catch (err) { + assert(err.message.includes('Invalid number of parameters for "newHeads"')); + assert(err.message.includes('Got 1 expected 0')); + } + }); + + it('errors when the provider is not set (callback)', function (done) { + web3 = new Web3(); + + web3.eth.subscribe('newBlockHeaders', function (err, result) { + assert(err.message.includes('No provider set')); + done(); + }); + }); + + it('errors when the provider is not set (.on("error"))', function (done) { + web3 = new Web3(); + + web3.eth + .subscribe('newBlockHeaders') + .once("error", function (err) { + assert(err.message.includes('No provider set')); + done(); + }); + }); + + it('errors when the provider does not support subscriptions (callback)', function (done) { + web3 = new Web3('http://localhost:' + port); + + web3.eth.subscribe('newBlockHeaders', function (err, result) { + assert(err.message.includes("provider doesn't support subscriptions: HttpProvider")); + done(); + }); + }); + + it('errors when the provider does not support subscriptions (.on("error"))', function (done) { + web3 = new Web3('http://localhost:' + port); + + web3.eth + .subscribe('newBlockHeaders') + .once("error", function (err) { + assert(err.message.includes("provider doesn't support subscriptions: HttpProvider")); + done(); + }); + }); + + it('errors when the `eth_subscribe` request got send, the reponse isnt returned from the node, and the connection does get closed in the mean time', async function () { + await pify(server.close)(); + + return new Promise(async function (resolve) { + web3.eth + .subscribe('newBlockHeaders') + .once('error', function (err) { + assert(err.message.includes('CONNECTION ERROR: Couldn\'t connect to node on WS')); + resolve(); + }); + }); + }); + + it('errors when the subscription got established (is running) and the connection does get closed', function () { + return new Promise(async function (resolve) { + web3.eth + .subscribe('newBlockHeaders') + .once('data', async function () { + await pify(server.close)(); + }) + .once('error', function (err) { + assert(err.message.includes('CONNECTION ERROR')); + assert(err.message.includes('close code `1006`')); + assert(err.message.includes('Connection dropped by remote peer.')); + resolve(); + }); + }); + }); + + it('auto reconnects and keeps the subscription running', function () { + this.timeout(6000); + + web3.setProvider(new Web3.providers.WebsocketProvider('ws://localhost:' + port, {reconnect: {auto: true}})); + + return new Promise(async function (resolve) { + // Stage 0: + let stage = 0; + + web3.eth + .subscribe('newBlockHeaders') + .on('data', function (result) { + assert(result.parentHash); + + // Exit point, flag set below + if (stage === 1) { + web3.currentProvider.disconnect(); + this.removeAllListeners(); + resolve(); + } + }); + + // Stage 1: Close & re-open server + await pify(server.close)(); + server = ganache.server({port: port, blockTime: 1}); + await pify(server.listen)(port); + stage = 1; + }); + }); + + it('auto reconnects, keeps the subscription running and triggers the `connected` event listener twice', function () { + this.timeout(6000); + + web3.setProvider(new Web3.providers.WebsocketProvider('ws://localhost:' + port, {reconnect: {auto: true}})); + + return new Promise(async function (resolve) { + // Stage 0: + let stage = 0; + + web3.eth + .subscribe('newBlockHeaders') + .on('connected', function (result) { + assert(result); + + // Exit point, flag set below + if (stage === 1) { + web3.currentProvider.disconnect(); + this.removeAllListeners(); + resolve(); + } + }); + + // Stage 1: Close & re-open server + await pify(server.close)(); + server = ganache.server({port: port, blockTime: 1}); + await pify(server.listen)(port); + stage = 1; + }); + }); +}); diff --git a/test/helpers/FakeIpcProvider.js b/test/helpers/FakeIpcProvider.js index 7c48e4a40da..6f52cb6c667 100644 --- a/test/helpers/FakeIpcProvider.js +++ b/test/helpers/FakeIpcProvider.js @@ -31,6 +31,7 @@ var FakeIpcProvider = function IpcProvider() { this.error = []; this.validation = []; this.notificationCallbacks = []; + this.connected = true; }; @@ -67,6 +68,10 @@ FakeIpcProvider.prototype.on = function (type, callback) { } }; +FakeIpcProvider.prototype.reset = function () { + this.notificationCallbacks = []; +}; + FakeIpcProvider.prototype.getResponseOrError = function (type, payload) { var _this = this; var response; diff --git a/test/helpers/test.utils.js b/test/helpers/test.utils.js index e4006ff27e8..67922e03aff 100644 --- a/test/helpers/test.utils.js +++ b/test/helpers/test.utils.js @@ -51,6 +51,11 @@ var getWebsocketPort = function(){ return ( process.env.GANACHE || global.window ) ? 8545 : 8546; } +// Delay +var waitSeconds = async function(seconds = 0){ + return new Promise(resolve => setTimeout(() => resolve(), seconds * 1000)) +} + module.exports = { methodExists: methodExists, propertyExists: propertyExists, @@ -58,5 +63,5 @@ module.exports = { extractReceipt: extractReceipt, getWeb3: getWeb3, getWebsocketPort: getWebsocketPort, + waitSeconds: waitSeconds }; - diff --git a/test/websocket.ganache.js b/test/websocket.ganache.js new file mode 100644 index 00000000000..b385f5abdba --- /dev/null +++ b/test/websocket.ganache.js @@ -0,0 +1,413 @@ +const assert = require('assert'); +const ganache = require('ganache-cli'); +const pify = require('pify'); +const utils = require('./helpers/test.utils'); +const Web3 = utils.getWeb3(); + +describe('WebsocketProvider (ganache)', function () { + let web3; + let server; + const host = 'ws://localhost:'; + const port = 8545; + + afterEach(async function () { + // Might already be closed.. + try { + await pify(server.close)(); + } catch (err) { + } + }); + + // This test's error is fired by the request queue checker in the onClose handler + it('errors when there is no connection', async function () { + web3 = new Web3(host + 8777); + + try { + await web3.eth.getBlockNumber(); + assert.fail(); + } catch (err) { + assert(err.code, 1006); + assert(err.reason, 'connection failed'); + assert(err.message.includes('connection not open on send')); + } + }); + + // Here, the first error (try/caught) is fired by the request queue checker in + // the onClose handler. The second error is fired by the readyState check in .send + it('errors when requests continue after socket closed', async function () { + web3 = new Web3(host + 8777); + + try { await web3.eth.getBlockNumber(); } catch (err) { + assert(err.message.includes('connection not open on send')); + assert(err.code, 1006); + assert(err.reason, 'connection failed'); + + try { + await web3.eth.getBlockNumber(); + assert.fail(); + } catch (err){ + assert(err.message.includes('connection not open on send')); + assert(typeof err.code === 'undefined'); + assert(typeof err.reason === 'undefined'); + } + } + }); + + it('errors after client has disconnected', async function () { + server = ganache.server({port: port}); + await pify(server.listen)(port); + + web3 = new Web3(new Web3.providers.WebsocketProvider(host + port)); + + // Verify connection and disconnect + await web3.eth.getBlockNumber(); + web3.currentProvider.disconnect(); + + try { + await web3.eth.getBlockNumber(); + assert.fail(); + } catch(err){ + assert(err.message.includes('connection not open on send')); + assert(typeof err.code === 'undefined'); + assert(typeof err.reason === 'undefined'); + } + }); + + it('can connect after being disconnected', async function () { + server = ganache.server({port: port}); + await pify(server.listen)(port); + + web3 = new Web3(new Web3.providers.WebsocketProvider(host + port)); + + // Verify connection and disconnect + await web3.eth.getBlockNumber(); + web3.currentProvider.disconnect(); + + try { await web3.eth.getBlockNumber() } catch(e){} + + web3.currentProvider.connect(); + + // This test fails unless there's a brief delay after + // connecting again... + await new Promise(resolve => { + setTimeout(async function(){ + const blockNumber = await web3.eth.getBlockNumber(); + assert(blockNumber === 0); + resolve(); + },100) + }); + }); + + it('supports subscriptions', async function () { + assert(web3.eth.currentProvider.supportsSubscriptions()); + }); + + it('times out when connection is lost mid-chunk', async function () { + this.timeout(5000); + server = ganache.server({port: port}); + await pify(server.listen)(port); + + web3 = new Web3( + new Web3.providers.WebsocketProvider( + host + port, + {timeout: 1000} + ) + ); + + await new Promise(resolve => { + web3.currentProvider.once('error', function(err){ + assert(err.message.includes('CONNECTION TIMEOUT: timeout of 1000 ms achived')); + resolve(); + }); + + web3.currentProvider._parseResponse('abc|--|dedf'); + }); + }); + + it('manually reconnecting', function () { + this.timeout(6000); + + return new Promise(async function (resolve) { + let stage = 0; + server = ganache.server({port: port}); + await pify(server.listen)(port); + + web3 = new Web3(new Web3.providers.WebsocketProvider(host + port)); + + web3.currentProvider.on('connect', async function () { + if (stage === 0) { + web3.currentProvider.reconnect(); + stage = 1; + } else { + await pify(server.close)(); + this.removeAllListeners(); + resolve(); + } + }); + }); + }); + + it('calling of reconnect with auto-reconnecting activated', function () { + this.timeout(6000); + + return new Promise(async function (resolve) { + let stage = 0; + server = ganache.server({port: port}); + await pify(server.listen)(port); + + web3 = new Web3(new Web3.providers.WebsocketProvider( + host + port, {reconnect: {auto: true}} + ) + ); + + web3.currentProvider.on('connect', async function () { + if (stage === 0) { + web3.currentProvider.reconnect(); + stage = 1; + } else { + await pify(server.close)(); + this.removeAllListeners(); + resolve(); + } + }); + }); + }); + + it('automatically connects as soon as the WS socket of the node is running', function () { + return new Promise(async function (resolve) { + web3 = new Web3( + new Web3.providers.WebsocketProvider( + host + port, + {reconnect: {auto: true}} + ) + ); + + web3.currentProvider.once('connect', async function () { + await pify(server.close)(); + resolve(); + }); + + server = ganache.server({port: port}); + await pify(server.listen)(port); + }); + }); + + it('reached the max. configured attempts and throws the expected error', function () { + this.timeout(6000); + + return new Promise(async function (resolve) { + server = ganache.server({port: port}); + await pify(server.listen)(port); + + web3 = new Web3( + new Web3.providers.WebsocketProvider( + host + port, + {reconnect: {auto: true, maxAttempts: 1}} + ) + ); + + web3.currentProvider.once('connect', async function () { + await pify(server.close)(); + }); + + web3.currentProvider.once('error', function (error) { + assert(error.message.includes('Maximum number of reconnect attempts reached!')); + resolve(); + }); + }); + }); + + it('allows disconnection when reconnect is enabled', function () { + this.timeout(6000); + + return new Promise(async function (resolve, reject) { + server = ganache.server({port: port}); + await pify(server.listen)(port); + + web3 = new Web3( + new Web3.providers.WebsocketProvider( + host + port, + {reconnect: {auto: true, maxAttempts: 1}} + ) + ); + + web3.currentProvider.once('connect', async function () { + web3.currentProvider.disconnect(); + + try { + await web3.eth.getBlockNumber(); + assert.fail(); + } catch (err) { + await pify(server.close)(); + assert(err.message.includes('connection not open on send')); + assert(typeof err.code === 'undefined'); + assert(typeof err.reason === 'undefined'); + resolve(); + } + }); + }); + }); + + // This test fails - the logic running in reconnect timeout doesn't know about the disconnect? + it.skip('allows disconnection on lost connection, when reconnect is enabled', function () { + this.timeout(6000); + let stage = 0; + + return new Promise(async function (resolve, reject) { + server = ganache.server({port: port}); + await pify(server.listen)(port); + + web3 = new Web3( + new Web3.providers.WebsocketProvider( + host + port, + {reconnect: {auto: true, maxAttempts: 1}} + ) + ); + + //Shutdown server + web3.currentProvider.on('connect', async function () { + // Stay isolated, just in case; + if (stage === 0){ + await pify(server.close)(); + web3.currentProvider.disconnect(); + stage = 1; + } + }); + + web3.currentProvider.on('error', function (error) { + assert(error.message.includes('Maximum number of reconnect attempts reached!')); + reject(new Error('Could not disconnect...')); + }); + }); + }); + + it('uses the custom configured delay on re-connect', function () { + let timeout; + this.timeout(4000); + + return new Promise(async function (resolve, reject) { + server = ganache.server({port: port}); + await pify(server.listen)(port); + + web3 = new Web3( + new Web3.providers.WebsocketProvider( + host + port, + {reconnect: {auto: true, delay: 3000, maxAttempts: 1}} + ) + ); + + web3.currentProvider.once('connect', async function () { + await pify(server.close)(); + timeout = setTimeout(function () { + reject(new Error('Test Failed: Configured delay is not applied!')); + }, 3100); + }); + + web3.currentProvider.once('reconnect', function () { + clearTimeout(timeout); + resolve(); + }); + }); + }); + + + it('clears pending requests on maxAttempts failed reconnection', function () { + this.timeout(6000); + + return new Promise(async function (resolve) { + server = ganache.server({port: port}); + await pify(server.listen)(port); + + web3 = new Web3( + new Web3.providers.WebsocketProvider( + host + port, + {reconnect: {auto: true, maxAttempts: 1}} + ) + ); + + web3.currentProvider.once('connect', async function () { + await pify(server.close)(); + }); + + web3.currentProvider.once('reconnect', async function () { + try { + await web3.eth.getBlockNumber(); + assert.fail(); + } catch (err) { + assert(err.message.includes('Maximum number of reconnect attempts')); + resolve(); + } + }); + }); + }); + + it('queues requests made while connection is lost / executes on reconnect', function () { + this.timeout(10000); + let stage = 0; + + return new Promise(async function (resolve) { + server = ganache.server({port: port}); + await pify(server.listen)(port); + + web3 = new Web3( + new Web3.providers.WebsocketProvider( + host + port, + {reconnect: {auto: true, delay: 2000, maxAttempts: 5}} + ) + ); + + web3.currentProvider.on('connect', async function () { + if (stage === 0){ + await pify(server.close)(); + stage = 1; + } + }); + + setTimeout(async function(){ + assert(stage === 1); + + const deferred = web3.eth.getBlockNumber(); + + server = ganache.server({port: port}); + await pify(server.listen)(port); + + const blockNumber = await deferred; + assert(blockNumber === 0); + + web3.currentProvider.removeAllListeners(); + resolve(); + },2500); + }); + }); + + it('errors when failing to reconnect after data is lost mid-chunk', async function () { + this.timeout(7000); + server = ganache.server({port: port}); + await pify(server.listen)(port); + + web3 = new Web3( + new Web3.providers.WebsocketProvider( + host + port, + { + timeout: 1000, + reconnect: { + auto: true, + delay: 2000, + maxAttempts: 1, + onTimeout: true + } + } + ) + ); + + await new Promise(async resolve => { + web3.currentProvider.once('error', function(err){ + assert(err.message.includes('Maximum number of reconnect attempts reached')); + resolve(); + }); + + await pify(server.close)(); + web3.currentProvider._parseResponse('abc|--|dedf'); + }); + }); +});