UNPKG

caver-js

Version:

caver-js is a JavaScript API library that allows developers to interact with a Klaytn node

419 lines (351 loc) 11.8 kB
/* Modifications copyright 2018 The caver-js Authors This file is part of web3.js. web3.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. web3.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 web3.js. If not, see <http://www.gnu.org/licenses/>. This file is derived from web3.js/packages/web3-providers-ws/src/index.js (2019/06/12). Modified and improved for the caver-js development. */ /** @file WebsocketProvider.js * @authors: * Fabian Vogelsteller <fabian@ethereum.org> * @date 2017 */ const EventEmitter = require('eventemitter3') const Ws = require('websocket').w3cwebsocket const helpers = require('./helpers') const errors = require('../../../caver-core-helpers').errors /** * @param {string} url * @param {Object} options * * @constructor */ const WebsocketProvider = function WebsocketProvider(url, options) { EventEmitter.call(this) options = options || {} this.url = url this._customTimeout = options.timeout || 1000 * 15 this.headers = options.headers || {} this.protocol = options.protocol || undefined this.reconnectOptions = { 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 const parsedURL = helpers.parseURL(url) if (parsedURL.username && parsedURL.password) { this.headers.authorization = `Basic ${helpers.btoa(`${parsedURL.username}:${parsedURL.password}`)}` } // 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) { this.headers.authorization = `Basic ${helpers.btoa(parsedURL.auth)}` } // 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, }) 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) { const _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 } let id = result.id // get the id which matches the returned id if (Array.isArray(result)) { id = result[0].id } if (_this.responseQueue.has(id)) { if (_this.responseQueue.get(id).callback !== undefined) { _this.responseQueue.get(id).callback(false, result) } _this.responseQueue.delete(id) } }) } /** * 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) { const _this = this this.requestQueue.forEach(function(request, key) { _this.send(request.payload, request.callback) _this.requestQueue.delete(key) }) } } /** * Listener for the `close` event of the underlying WebSocket object * * @method _onClose * * @returns {void} */ WebsocketProvider.prototype._onClose = function(event) { const _this = this 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) }) } if (this.responseQueue.size > 0) { this.responseQueue.forEach(function(request, key) { request.callback(errors.InvalidConnection('on WS', event)) _this.responseQueue.delete(key) }) } this._removeSocketListeners() this.removeAllListeners() } /** * 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 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) } /** * Will parse the response and make an array out of it. * * @method _parseResponse * * @param {String} data * * @returns {Array} */ WebsocketProvider.prototype._parseResponse = function(data) { const _this = this const returnValues = [] // DE-CHUNKER const dechunkedData = data .replace(/\}[\n\r]?\{/g, '}|--|{') // }{ .replace(/\}\][\n\r]?\[\{/g, '}]|--|[{') // }][{ .replace(/\}[\n\r]?\[\{/g, '}|--|[{') // }[{ .replace(/\}\][\n\r]?\{/g, '}]|--|{') // }]{ .split('|--|') dechunkedData.forEach(function(d) { // prepend the last chunk if (_this.lastChunk) d = _this.lastChunk + d let result = null try { result = JSON.parse(d) } catch (e) { _this.lastChunk = d // start timeout to cancel all requests clearTimeout(_this.lastChunkTimeout) _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 } // cancel timeout and set chunk to null clearTimeout(_this.lastChunkTimeout) _this.lastChunk = null if (result) returnValues.push(result) }) return returnValues } /** * 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.send = function(payload, callback) { const _this = this let id = payload.id const request = { payload: payload, callback: callback } if (Array.isArray(payload)) { id = payload[0].id } if (this.connection.readyState === this.connection.CONNECTING || this.reconnecting) { this.requestQueue.set(id, request) return } if (this.connection.readyState !== this.connection.OPEN) { this.requestQueue.delete(id) this.emit(this.ERROR, errors.ConnectionNotOpenError()) request.callback(errors.ConnectionNotOpenError()) return } this.responseQueue.set(id, request) this.requestQueue.delete(id) try { this.connection.send(JSON.stringify(request.payload)) } catch (error) { request.callback(error) _this.responseQueue.delete(id) } } /** * Resets the providers, clears all callbacks * * @method reset * * @returns {void} */ WebsocketProvider.prototype.reset = function() { this.responseQueue.clear() this.requestQueue.clear() this.removeAllListeners() this._removeSocketListeners() this._addSocketListeners() } /** * Closes the current connection with the given code and reason arguments * * @method disconnect * * @param {number} code * @param {string} reason * * @returns {void} */ WebsocketProvider.prototype.disconnect = function(code, reason) { this._removeSocketListeners() this.connection.close(code || 1000, reason) } /** * Returns the desired boolean. * * @method supportsSubscriptions * * @returns {boolean} */ WebsocketProvider.prototype.supportsSubscriptions = function() { return true } /** * Removes the listeners and reconnects to the socket. * * @method reconnect * * @returns {void} */ WebsocketProvider.prototype.reconnect = function() { const _this = this this.reconnecting = true if (this.responseQueue.size > 0) { this.responseQueue.forEach(function(request, key) { request.callback(errors.PendingRequestsOnReconnectingError()) _this.responseQueue.delete(key) }) } 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) return } this.emit(this.ERROR, errors.MaxAttemptsReachedOnReconnectingError()) this.reconnecting = false if (this.requestQueue.size > 0) { this.requestQueue.forEach(function(request, key) { request.callback(errors.MaxAttemptsReachedOnReconnectingError()) _this.requestQueue.delete(key) }) } } module.exports = WebsocketProvider