caver-js
Version:
caver-js is a JavaScript API library that allows developers to interact with a Kaia node
347 lines (295 loc) • 10.6 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-core-subscriptions/src/subscription.js (2019/06/12).
Modified and improved for the caver-js development.
*/
/**
* @file subscription.js
* @author Fabian Vogelsteller <fabian@ethereum.org>
* @date 2017
*/
const _ = require('lodash')
const EventEmitter = require('eventemitter3')
const errors = require('../../caver-core-helpers').errors
/**
* @classdesc A subscription class implemented to subscribe the specific events in the blockchain.
* @class
* @hideconstructor
*/
function Subscription(options) {
EventEmitter.call(this)
this.id = null
this.callback = null
this.arguments = null
this._reconnectIntervalId = null
this.options = {
subscription: options.subscription,
type: options.type,
requestManager: options.requestManager,
}
}
Subscription.prototype = Object.create(EventEmitter.prototype, {
constructor: { value: Subscription },
})
/**
* Should be used to extract callback from array of arguments. Modifies input param
*
* @ignore
* @method _extractCallback
* @param {Array} arguments
* @return {Function|Null} callback, if exists
*/
Subscription.prototype._extractCallback = function(args) {
if (_.isFunction(args[args.length - 1])) {
return args.pop() // modify the args array!
}
}
/**
* Should be called to check if the number of arguments is correct
*
* @ignore
* @method _validateArgs
* @param {Array} arguments
* @throws {Error} if it is not
*/
Subscription.prototype._validateArgs = function(args) {
let subscription = this.options.subscription
if (!subscription) {
subscription = {}
}
if (!subscription.params) {
subscription.params = 0
}
if (args.length !== subscription.params) {
throw errors.InvalidNumberOfParams(args.length, subscription.params + 1, args[0])
}
}
/**
* Should be called to format input args of method
*
* @ignore
* @method _formatInput
* @param {Array}
* @return {Array}
*/
Subscription.prototype._formatInput = function(args) {
const subscription = this.options.subscription
if (!subscription) {
return args
}
if (!subscription.inputFormatter) {
return args
}
const formattedArgs = subscription.inputFormatter.map(function(formatter, index) {
return formatter ? formatter(args[index]) : args[index]
})
return formattedArgs
}
/**
* Should be called to format output(result) of method
*
* @ignore
* @method _formatOutput
* @param {Object}
* @return {Object}
*/
Subscription.prototype._formatOutput = function(result) {
const subscription = this.options.subscription
return subscription && subscription.outputFormatter && result ? subscription.outputFormatter(result) : result
}
/**
* Should create payload from given input args
*
* @ignore
* @method _toPayload
* @param {Array} args
* @return {Object}
*/
Subscription.prototype._toPayload = function(args) {
let params = []
this.callback = this._extractCallback(args)
if (!this.subscriptionMethod) {
this.subscriptionMethod = args.shift()
if (this.options.subscription.subscriptionName) {
this.subscriptionMethod = this.options.subscription.subscriptionName
}
}
if (!this.arguments) {
this.arguments = this._formatInput(args)
this._validateArgs(this.arguments)
args = [] // make empty after validation
}
// re-add subscriptionName
params.push(this.subscriptionMethod)
params = params.concat(this.arguments)
if (args.length) {
throw new Error('Only a callback is allowed as parameter on an already instantiated subscription.')
}
return {
method: `${this.options.type}_subscribe`,
params: params,
}
}
/**
* Unsubscribes and clears callbacks.
*
* @return {Object}
*/
Subscription.prototype.unsubscribe = function(callback) {
this.options.requestManager.removeSubscription(this.id, callback)
this.id = null
this.removeAllListeners()
clearInterval(this._reconnectIntervalId)
}
/**
* Subscribes and watches for changes
*
* @param {String} subscription the subscription
* @param {Object} options the options object with address topics and fromBlock
* @return {Object}
*/
Subscription.prototype.subscribe = function() {
const _this = this
const args = Array.prototype.slice.call(arguments)
const payload = this._toPayload(args)
if (!payload) {
return this
}
if (!this.options.requestManager.provider) {
const err1 = new Error('No provider set.')
this.callback(err1, null, this)
/**
* Subscription 'error' event.
*
* @event Subscription#error
* @type {Error}
*/
this.emit('error', err1)
return this
}
if (!this.options.requestManager.provider.on) {
const 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)
return this
}
if (this.id) {
this.unsubscribe()
}
this.options.params = payload.params[1]
// get past logs, if fromBlock is available
if (
payload.params[0] === 'logs' &&
_.isObject(payload.params[1]) &&
Object.prototype.hasOwnProperty.call(payload.params[1], 'fromBlock') &&
isFinite(payload.params[1].fromBlock)
) {
// send the subscription request
// copy the params to avoid race-condition with deletion below this block
const blockParams = { ...payload.params[1] }
this.options.requestManager.send(
{
method: 'klay_getLogs',
params: [blockParams],
},
function(err, logs) {
if (!err) {
logs.forEach(function(log) {
const output = _this._formatOutput(log)
_this.callback(null, output, _this)
/**
* Subscription 'data' event.
*
* @event Subscription#data
* @type {object}
*/
_this.emit('data', output)
})
// TODO subscribe here? after the past logs?
} else {
_this.callback(err, null, _this)
_this.emit('error', err)
}
}
)
}
// create subscription
// TODO move to separate function? so that past logs can go first?
if (typeof payload.params[1] === 'object') {
delete payload.params[1].fromBlock
}
this.options.requestManager.send(payload, function(err, result) {
if (!err && result) {
_this.id = result
/**
* Subscription 'connected' event.
*
* @event Subscription#connected
* @type {string}
*/
_this.emit('connected', result)
// call callback on notifications
_this.options.requestManager.addSubscription(_this.id, payload.params[0], _this.options.type, function(error, ret) {
if (!error) {
if (!_.isArray(ret)) {
ret = [ret]
}
ret.forEach(function(resultItem) {
const output = _this._formatOutput(resultItem)
if (_.isFunction(_this.options.subscription.subscriptionHandler)) {
return _this.options.subscription.subscriptionHandler.call(_this, output)
}
_this.emit('data', output)
// call the callback, last so that unsubscribe there won't affect the emit above
if (_.isFunction(_this.callback)) {
_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', error)
// call the callback, last so that unsubscribe there won't affect the emit above
if (_.isFunction(_this.callback)) {
_this.callback(error, null, _this)
}
}
})
} else if (_.isFunction(_this.callback)) {
_this.callback(err, null, _this)
_this.emit('error', err)
} else {
// emit the event even if no callback was provided
_this.emit('error', err)
}
})
// return an object to cancel the subscription
return this
}
module.exports = Subscription