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
JavaScript
/*
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