leasehold-chain
Version:
Leasehold sidechain
256 lines (230 loc) • 6.43 kB
JavaScript
/*
* Copyright © 2019 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*/
'use strict';
const _ = require('lodash');
const jobsQueue = require('../utils/jobs_queue');
/**
* Main Broadcaster logic.
* Initializes variables, sets Broadcast routes and timer based on
* broadcast interval from config file.
*
* @class
* @memberof logic
* @see Parent: {@link logic}
* @requires lodash
* @requires utils/jobs_queue
* @param {Object} broadcasts
* @param {boolean} force
* @param {Transaction} transaction - Transaction instance
* @param {Object} logger
* @todo Add description for the params
*/
class Broadcaster {
constructor(nonce, broadcasts, transactionPool, logger, channel, storage, moduleAlias) {
this.logger = logger;
this.transactionPool = transactionPool;
this.config = broadcasts;
this.channel = channel;
this.nonce = nonce;
this.storage = storage;
this.moduleAlias = moduleAlias;
this.queue = [];
// Broadcast routes
this.routes = [
{
path: `${this.moduleAlias}:postTransactions`,
collection: 'transactions',
object: 'transaction',
},
];
if (this.config.active) {
jobsQueue.register(
'broadcasterReleaseQueue',
async () => this.releaseQueue(),
this.config.broadcastInterval,
);
} else {
this.logger.info(
'Broadcasting data disabled by user through config.json',
);
}
}
/**
* broadcast to network.
*
* @param {Object} params
* @param {Object} options
* @returns {Promise}
* @throws {Error}
* @todo Add description for the params
*/
async broadcast(params, { api: event, data }) {
// Broadcast using Elements P2P library via network module
const wrappedData = {
...data,
nonce: this.nonce,
};
await this.channel.invoke('network:emit', {
event,
data: wrappedData,
});
}
/**
* Adds new object {params, options} to queue array.
*
* @param {Object} params
* @param {Object} options
* @returns {Object[]} Queue private variable with new data
* @todo Add description for the params
*/
enqueue(params, options) {
options.immediate = false;
return this.queue.push({ params, options });
}
/**
* Filters private queue based on broadcasts.
*
* @private
* @returns {Promise} null, boolean|undefined
* @todo Add description for the params
*/
async filterQueue() {
this.logger.debug(`Broadcasts before filtering: ${this.queue.length}`);
const transactionIdsNotInPool = [];
this.queue = this.queue.filter(broadcast => {
if (broadcast.options.immediate) {
return false;
}
if (broadcast.options.data) {
let transactionId;
if (broadcast.options.data.transaction) {
// Look for a transaction of a given "id"
transactionId = broadcast.options.data.transaction.id;
} else if (broadcast.options.data.signature) {
// Look for a corresponding "transactionId" of a given signature
({ transactionId } = broadcast.options.data.signature);
}
if (!transactionId) {
return false;
}
// Broadcast if transaction is in transaction pool
if (!this.transactionPool.transactionInPool(transactionId)) {
transactionIdsNotInPool.push(transactionId);
}
return true;
}
return true;
});
const persistedTransactionIds = (await Promise.all(
transactionIdsNotInPool.map(async transactionId => {
try {
const isPersisted = await this.storage.entities.Transaction.isPersisted(
{
id: transactionId,
},
);
return {
transactionId,
isPersisted,
};
} catch (e) {
// if there is an error for transaction id then remove it from the broadcasts
return {
transactionId,
isPersisted: true,
};
}
}),
))
.filter(({ isPersisted }) => isPersisted)
.map(({ transactionId }) => transactionId);
this.queue = this.queue.filter(broadcast => {
if (broadcast.options.data) {
let transactionId;
if (broadcast.options.data.transaction) {
// Look for a transaction of a given "id"
transactionId = broadcast.options.data.transaction.id;
} else if (broadcast.options.data.signature) {
// Look for a corresponding "transactionId" of a given signature
({ transactionId } = broadcast.options.data.signature);
}
return !persistedTransactionIds.includes(transactionId);
}
return true;
});
this.logger.debug(`Broadcasts after filtering: ${this.queue.length}`);
return null;
}
/**
* Groups broadcasts by api.
*
* @private
* @param {Object} broadcasts
* @returns {Object[]} Squashed routes
* @todo Add description for the params
*/
squashQueue(broadcasts) {
const grouped = _.groupBy(broadcasts, broadcast => broadcast.options.api);
const squashed = [];
this.routes.forEach(route => {
if (Array.isArray(grouped[route.path])) {
const data = {};
data[route.collection] = grouped[route.path]
.map(broadcast => broadcast.options.data[route.object])
.filter(Boolean);
squashed.push({
options: { api: route.path, data },
immediate: false,
});
}
});
return squashed;
}
/**
* Releases enqueued broadcasts:
* - filterQueue
* - squashQueue
* - broadcast
*
* @private
* @returns {Promise}
* @throws {Error}
* @todo Add description for the params
*/
// eslint-disable-next-line class-methods-use-this
async releaseQueue() {
this.logger.trace('Releasing enqueued broadcasts');
if (!this.queue.length) {
this.logger.trace('Queue empty');
return null;
}
try {
await this.filterQueue();
const broadcasts = this.queue.splice(0, this.config.releaseLimit);
const squashedBroadcasts = this.squashQueue(broadcasts);
await Promise.all(
squashedBroadcasts.map(({ params, options }) =>
this.broadcast(params, options),
),
);
return this.logger.info(
`Broadcasts released: ${squashedBroadcasts.length}`,
);
} catch (err) {
this.logger.error('Failed to release broadcast queue', err);
throw err;
}
}
}
module.exports = Broadcaster;