ethstats-server/lib/history.js

662 lines
14 KiB
JavaScript
Raw Normal View History

2015-04-17 11:10:20 +02:00
var _ = require('lodash');
2015-04-24 02:08:12 +02:00
var d3 = require('d3');
2015-04-17 11:10:20 +02:00
2015-06-09 18:28:29 +02:00
var MAX_HISTORY = 2000;
2015-04-24 05:37:52 +02:00
2015-06-09 01:40:59 +02:00
var MAX_PEER_PROPAGATION = 40;
2015-04-24 05:23:26 +02:00
var MIN_PROPAGATION_RANGE = 0;
2015-05-13 01:44:47 +02:00
var MAX_PROPAGATION_RANGE = 10000;
2015-04-24 05:37:52 +02:00
2015-06-09 18:28:29 +02:00
var MAX_UNCLES = 1000;
2015-04-24 05:37:52 +02:00
var MAX_UNCLES_PER_BIN = 25;
2015-04-23 15:17:31 +02:00
var MAX_BINS = 40;
2015-04-17 11:10:20 +02:00
var History = function History(data)
{
this._items = [];
2015-06-09 01:40:59 +02:00
this._callback = null;
2015-04-17 11:10:20 +02:00
}
2015-06-09 18:28:29 +02:00
History.prototype.add = function(block, id, trusted, addingHistory)
2015-04-17 11:10:20 +02:00
{
2015-06-09 01:40:59 +02:00
var changed = false;
2015-04-29 11:00:01 +02:00
if( !_.isUndefined(block) && !_.isUndefined(block.number) && !_.isUndefined(block.uncles) && !_.isUndefined(block.transactions) && !_.isUndefined(block.difficulty) && block.number > 0 )
2015-04-17 11:10:20 +02:00
{
2015-07-29 19:15:45 +02:00
trusted = (process.env.LITE === 'true' ? true : trusted);
2015-04-28 01:50:28 +02:00
var historyBlock = this.search(block.number);
2015-06-09 18:28:29 +02:00
var forkIndex = -1;
2015-04-17 11:10:20 +02:00
2015-04-29 07:49:43 +02:00
var now = _.now();
2015-06-09 18:28:29 +02:00
block.trusted = trusted;
2015-04-28 01:50:28 +02:00
block.arrived = now;
block.received = now;
block.propagation = 0;
2015-06-09 18:28:29 +02:00
block.fork = 0;
2015-04-28 01:50:28 +02:00
2015-04-29 07:49:43 +02:00
if( historyBlock )
2015-04-17 11:10:20 +02:00
{
2015-06-09 18:28:29 +02:00
// We already have a block with this height in collection
// Check if node already checked this block height
2015-04-29 11:00:01 +02:00
var propIndex = _.findIndex( historyBlock.propagTimes, { node: id } );
2015-04-28 01:50:28 +02:00
2015-06-09 18:28:29 +02:00
// Check if node already check a fork with this height
forkIndex = compareForks(historyBlock, block);
2015-04-29 07:49:43 +02:00
if( propIndex === -1 )
2015-04-28 01:50:28 +02:00
{
2015-06-09 18:28:29 +02:00
// Node didn't submit this block before
if( forkIndex >= 0 && !_.isUndefined(historyBlock.forks[forkIndex]) )
{
// Found fork => update data
block.arrived = historyBlock.forks[forkIndex].arrived;
block.propagation = now - historyBlock.forks[forkIndex].received;
}
else
{
// No fork found => add a new one
2015-06-17 04:17:59 +02:00
var prevBlock = this.prevMaxBlock(block.number);
if( prevBlock )
{
block.time = Math.max(block.arrived - prevBlock.block.arrived, 0);
if(block.number < this.bestBlock().height)
block.time = Math.max((block.timestamp - prevBlock.block.timestamp) * 1000, 0);
}
else
{
block.time = 0;
}
2015-06-09 18:28:29 +02:00
forkIndex = historyBlock.forks.push(block) - 1;
historyBlock.forks[forkIndex].fork = forkIndex;
}
2015-04-17 11:10:20 +02:00
2015-06-09 18:28:29 +02:00
// Push propagation time
2015-04-29 07:49:43 +02:00
historyBlock.propagTimes.push({
node: id,
2015-06-09 18:28:29 +02:00
trusted: trusted,
fork: forkIndex,
2015-04-29 07:49:43 +02:00
received: now,
propagation: block.propagation
});
2015-04-28 01:50:28 +02:00
}
else
{
2015-06-09 18:28:29 +02:00
// Node submited the block before
if( forkIndex >= 0 && !_.isUndefined(historyBlock.forks[forkIndex]) )
{
// Matching fork found => update data
block.arrived = historyBlock.forks[forkIndex].arrived;
if( forkIndex === historyBlock.propagTimes[propIndex].fork )
{
// Fork index is the same
block.received = historyBlock.propagTimes[propIndex].received;
block.propagation = historyBlock.propagTimes[propIndex].propagation;
}
else
{
// Fork index is different
historyBlock.propagTimes[propIndex].fork = forkIndex;
historyBlock.propagTimes[propIndex].propagation = block.propagation = now - historyBlock.forks[forkIndex].received;
}
2015-06-01 23:50:03 +02:00
2015-06-09 18:28:29 +02:00
}
else
2015-06-01 23:50:03 +02:00
{
2015-06-09 18:28:29 +02:00
// No matching fork found => replace old one
block.received = historyBlock.propagTimes[propIndex].received;
block.propagation = historyBlock.propagTimes[propIndex].propagation;
2015-06-17 04:17:59 +02:00
var prevBlock = this.prevMaxBlock(block.number);
if( prevBlock )
{
block.time = Math.max(block.arrived - prevBlock.block.arrived, 0);
if(block.number < this.bestBlock().height)
block.time = Math.max((block.timestamp - prevBlock.block.timestamp) * 1000, 0);
}
else
{
block.time = 0;
}
2015-06-09 18:28:29 +02:00
forkIndex = historyBlock.forks.push(block) - 1;
historyBlock.forks[forkIndex].fork = forkIndex;
2015-06-01 23:50:03 +02:00
}
2015-04-28 01:50:28 +02:00
}
2015-06-09 18:28:29 +02:00
if( trusted && !compareBlocks(historyBlock.block, historyBlock.forks[forkIndex]) )
{
// If source is trusted update the main block
historyBlock.forks[forkIndex].trusted = trusted;
historyBlock.block = historyBlock.forks[forkIndex];
}
block.fork = forkIndex;
changed = true;
2015-04-17 11:10:20 +02:00
}
else
{
2015-06-09 18:28:29 +02:00
// Couldn't find block with this height
// Getting previous max block
2015-04-28 05:51:04 +02:00
var prevBlock = this.prevMaxBlock(block.number);
2015-04-29 07:49:43 +02:00
if( prevBlock )
2015-04-28 05:51:04 +02:00
{
2015-05-11 19:02:21 +02:00
block.time = Math.max(block.arrived - prevBlock.block.arrived, 0);
2015-04-28 09:43:56 +02:00
if(block.number < this.bestBlock().height)
2015-05-11 19:02:21 +02:00
block.time = Math.max((block.timestamp - prevBlock.block.timestamp) * 1000, 0);
2015-04-28 05:51:04 +02:00
}
else
{
block.time = 0;
}
2015-04-29 07:49:43 +02:00
2015-06-09 02:57:56 +02:00
var item = {
height: block.number,
block: block,
2015-06-09 18:28:29 +02:00
forks: [block],
2015-06-09 02:57:56 +02:00
propagTimes: []
}
if( this._items.length === 0 || (this._items.length > 0 && block.number > this.worstBlockNumber()) || (this._items.length < MAX_HISTORY && block.number < this.bestBlockNumber() && addingHistory) )
2015-04-29 01:10:23 +02:00
{
2015-04-29 07:49:43 +02:00
item.propagTimes.push({
node: id,
2015-06-09 18:28:29 +02:00
trusted: trusted,
fork: 0,
2015-04-29 07:49:43 +02:00
received: now,
propagation: block.propagation
});
2015-04-29 01:10:23 +02:00
this._save(item);
2015-06-09 01:40:59 +02:00
changed = true;
2015-04-29 01:10:23 +02:00
}
2015-04-17 11:10:20 +02:00
}
2015-06-09 01:40:59 +02:00
return {
block: block,
changed: changed
};
2015-04-17 11:10:20 +02:00
}
2015-04-28 01:50:28 +02:00
return false;
2015-04-17 11:10:20 +02:00
}
2015-06-09 18:28:29 +02:00
function compareBlocks(block1, block2)
{
if( block1.hash !== block2.hash ||
block1.parentHash !== block2.parentHash ||
block1.sha3Uncles !== block2.sha3Uncles ||
block1.transactionsRoot !== block2.transactionsRoot ||
block1.stateRoot !== block2.stateRoot ||
block1.miner !== block2.miner ||
block1.difficulty !== block2.difficulty ||
2015-07-21 17:46:28 +02:00
block1.totalDifficulty !== block2.totalDifficulty)
2015-06-09 18:28:29 +02:00
return false;
return true;
}
function compareForks(historyBlock, block2)
{
if( _.isUndefined(historyBlock) )
return -1;
if( _.isUndefined(historyBlock.forks) || historyBlock.forks.length === 0 )
return -1;
for(var x = 0; x < historyBlock.forks.length; x++)
if(compareBlocks(historyBlock.forks[x], block2))
return x;
return -1;
}
2015-04-17 11:10:20 +02:00
History.prototype._save = function(block)
{
2015-04-28 09:43:56 +02:00
this._items.unshift(block);
2015-04-17 11:10:20 +02:00
2020-08-21 09:02:47 +02:00
this._items = _.orderBy( this._items, 'height', "desc" );
2015-05-27 11:22:17 +02:00
2015-04-29 07:49:43 +02:00
if(this._items.length > MAX_HISTORY)
{
2015-04-28 09:43:56 +02:00
this._items.pop();
2015-04-17 11:10:20 +02:00
}
}
History.prototype.clean = function(max)
{
if(max > 0 && this._items.length > 0 && max < this.bestBlockNumber())
{
console.log("MAX:", max);
console.log("History items before:", this._items.length);
this._items = _(this._items).filter(function(item) {
2015-06-09 18:28:29 +02:00
return (item.height <= max && item.block.trusted === false);
}).value();
console.log("History items after:", this._items.length);
}
}
2015-04-17 11:10:20 +02:00
History.prototype.search = function(number)
{
2015-04-29 11:00:01 +02:00
var index = _.findIndex( this._items, { height: number } );
2015-04-17 11:10:20 +02:00
if(index < 0)
return false;
return this._items[index];
}
2015-04-28 05:51:04 +02:00
History.prototype.prevMaxBlock = function(number)
{
2015-04-29 11:00:01 +02:00
var index = _.findIndex(this._items, function (item) {
2015-04-28 05:51:04 +02:00
return item.height < number;
});
if(index < 0)
return false;
return this._items[index];
}
History.prototype.bestBlock = function()
2015-04-17 11:10:20 +02:00
{
return _.maxBy(this._items, 'height');
2015-04-17 11:10:20 +02:00
}
History.prototype.bestBlockNumber = function()
{
var best = this.bestBlock();
if( !_.isUndefined(best) && !_.isUndefined(best.height) )
return best.height;
return 0;
}
2015-06-09 18:28:29 +02:00
History.prototype.worstBlock = function()
{
return _.minBy(this._items, 'height');
2015-06-09 18:28:29 +02:00
}
History.prototype.worstBlockNumber = function(trusted)
{
var worst = this.worstBlock();
if( !_.isUndefined(worst) && !_.isUndefined(worst.height) )
2015-06-09 18:28:29 +02:00
return worst.height;
return 0;
}
2015-04-17 11:10:20 +02:00
History.prototype.getNodePropagation = function(id)
{
2015-04-29 07:49:43 +02:00
var propagation = new Array( MAX_PEER_PROPAGATION );
var bestBlock = this.bestBlockNumber();
var lastBlocktime = _.now();
2015-04-23 15:17:31 +02:00
2015-04-17 11:10:20 +02:00
_.fill(propagation, -1);
2015-04-29 07:49:43 +02:00
var sorted = _( this._items )
2019-02-18 21:41:10 +01:00
.sortBy( 'height', false )
2015-04-29 07:49:43 +02:00
.slice( 0, MAX_PEER_PROPAGATION )
.forEach(function (item, key)
2015-04-17 11:10:20 +02:00
{
2015-04-29 07:49:43 +02:00
var index = MAX_PEER_PROPAGATION - 1 - bestBlock + item.height;
2015-04-23 15:17:31 +02:00
if(index >= 0)
2015-04-17 11:10:20 +02:00
{
var tmpPropagation = _.result(_.find(item.propagTimes, 'node', id), 'propagation', false);
if (_.result(_.find(item.propagTimes, 'node', id), 'propagation', false) !== false)
{
propagation[index] = tmpPropagation;
lastBlocktime = item.block.arrived;
}
else
{
propagation[index] = Math.max(0, lastBlocktime - item.block.arrived);
}
2015-04-17 11:10:20 +02:00
}
})
.reverse();
2015-04-17 11:10:20 +02:00
return propagation;
}
History.prototype.getBlockPropagation = function()
{
2015-04-23 15:17:31 +02:00
var propagation = [];
2015-04-28 17:44:41 +02:00
var avgPropagation = 0;
2015-04-17 11:10:20 +02:00
2015-04-29 07:49:43 +02:00
_.forEach(this._items, function (n, key)
2015-04-24 02:08:12 +02:00
{
2015-04-29 07:49:43 +02:00
_.forEach(n.propagTimes, function (p, i)
2015-04-17 11:10:20 +02:00
{
2015-05-13 01:44:47 +02:00
var prop = Math.min(MAX_PROPAGATION_RANGE, _.result(p, 'propagation', -1));
2015-04-24 02:08:12 +02:00
if(prop >= 0)
propagation.push(prop);
});
});
2015-04-28 17:44:41 +02:00
if(propagation.length > 0)
{
2015-04-29 07:49:43 +02:00
var avgPropagation = Math.round( _.sum(propagation) / propagation.length );
2015-04-28 17:44:41 +02:00
}
2015-04-24 02:08:12 +02:00
var data = d3.layout.histogram()
2015-04-29 07:49:43 +02:00
.frequency( false )
2015-05-13 01:44:47 +02:00
.range([ MIN_PROPAGATION_RANGE, MAX_PROPAGATION_RANGE ])
.bins( MAX_BINS )
2015-04-29 07:49:43 +02:00
( propagation );
2015-04-24 02:08:12 +02:00
var freqCum = 0;
2015-04-29 07:49:43 +02:00
var histogram = data.map(function (val) {
2015-04-24 02:08:12 +02:00
freqCum += val.length;
2015-04-29 07:49:43 +02:00
var cumPercent = ( freqCum / Math.max(1, propagation.length) );
return {
x: val.x,
dx: val.dx,
y: val.y,
frequency: val.length,
cumulative: freqCum,
cumpercent: cumPercent
};
2015-04-24 02:08:12 +02:00
});
2015-04-29 07:49:43 +02:00
return {
histogram: histogram,
avg: avgPropagation
};
2015-04-24 05:23:26 +02:00
}
2015-04-28 03:58:19 +02:00
History.prototype.getUncleCount = function()
2015-04-24 05:23:26 +02:00
{
2015-04-29 07:49:43 +02:00
var uncles = _( this._items )
2019-02-18 21:41:10 +01:00
.sortBy( 'height', false )
2015-06-17 03:45:46 +02:00
// .filter(function (item)
// {
// return item.block.trusted;
// })
2015-06-09 18:28:29 +02:00
.slice(0, MAX_UNCLES)
2015-04-29 07:49:43 +02:00
.map(function (item)
2015-04-24 05:23:26 +02:00
{
return item.block.uncles.length;
})
.value();
2015-04-29 07:49:43 +02:00
var uncleBins = _.fill( Array(MAX_BINS), 0 );
2015-04-24 05:23:26 +02:00
2015-04-29 07:49:43 +02:00
var sumMapper = function (array, key)
{
2015-04-24 05:23:26 +02:00
uncleBins[key] = _.sum(array);
return _.sum(array);
};
2015-04-29 07:49:43 +02:00
_.map(_.chunk( uncles, MAX_UNCLES_PER_BIN ), sumMapper);
2015-04-24 05:23:26 +02:00
return uncleBins;
2015-04-17 11:10:20 +02:00
}
2015-04-28 05:51:04 +02:00
History.prototype.getBlockTimes = function()
{
2015-04-29 07:49:43 +02:00
var blockTimes = _( this._items )
2019-02-18 21:41:10 +01:00
.sortBy( 'height', false )
2015-06-17 03:45:46 +02:00
// .filter(function (item)
// {
// return item.block.trusted;
// })
2015-04-28 05:51:04 +02:00
.slice(0, MAX_BINS)
.reverse()
2015-04-29 07:49:43 +02:00
.map(function (item)
2015-04-28 05:51:04 +02:00
{
return item.block.time / 1000;
2015-04-28 05:51:04 +02:00
})
.value();
return blockTimes;
}
History.prototype.getAvgBlocktime = function()
{
var blockTimes = _( this._items )
2019-02-18 21:41:10 +01:00
.sortBy( 'height', false )
// .filter(function (item)
// {
// return item.block.trusted;
// })
// .slice(0, MAX_BINS)
.reverse()
.map(function (item)
{
return item.block.time / 1000;
})
.value();
return _.sum(blockTimes) / (blockTimes.length === 0 ? 1 : blockTimes.length);
}
2015-08-06 12:22:47 +02:00
History.prototype.getGasLimit = function()
{
var gasLimitHistory = _( this._items )
2019-02-18 21:41:10 +01:00
.sortBy( 'height', false )
2015-08-06 12:54:10 +02:00
// .filter(function (item)
// {
// return item.block.trusted;
// })
2015-08-06 12:22:47 +02:00
.slice(0, MAX_BINS)
.reverse()
.map(function (item)
{
return item.block.gasLimit;
})
.value();
return gasLimitHistory;
}
2015-04-28 06:25:15 +02:00
History.prototype.getDifficulty = function()
{
2015-04-29 07:49:43 +02:00
var difficultyHistory = _( this._items )
2019-02-18 21:41:10 +01:00
.sortBy( 'height', false )
2015-06-09 18:28:29 +02:00
.filter(function (item)
{
return item.block.trusted;
})
2015-04-28 06:25:15 +02:00
.slice(0, MAX_BINS)
.reverse()
2015-04-29 07:49:43 +02:00
.map(function (item)
2015-04-28 06:25:15 +02:00
{
return item.block.difficulty;
})
.value();
return difficultyHistory;
}
2015-04-28 03:58:19 +02:00
History.prototype.getTransactionsCount = function()
2015-04-28 02:52:41 +02:00
{
2015-04-29 07:49:43 +02:00
var txCount = _( this._items )
2019-02-18 21:41:10 +01:00
.sortBy( 'height', false )
2015-06-09 18:28:29 +02:00
.filter(function (item)
{
return item.block.trusted;
})
2015-04-28 03:50:21 +02:00
.slice(0, MAX_BINS)
2015-04-28 02:52:41 +02:00
.reverse()
2015-04-29 07:49:43 +02:00
.map(function (item)
2015-04-28 02:52:41 +02:00
{
return item.block.transactions.length;
})
.value();
2015-04-28 03:02:12 +02:00
return txCount;
2015-04-28 02:52:41 +02:00
}
2015-04-28 03:58:19 +02:00
History.prototype.getGasSpending = function()
2015-04-28 03:49:43 +02:00
{
2015-04-29 07:49:43 +02:00
var gasSpending = _( this._items )
2019-02-18 21:41:10 +01:00
.sortBy( 'height', false )
2015-06-09 18:28:29 +02:00
.filter(function (item)
{
return item.block.trusted;
})
2015-04-28 03:50:21 +02:00
.slice(0, MAX_BINS)
2015-04-28 03:49:43 +02:00
.reverse()
2015-04-29 07:49:43 +02:00
.map(function (item)
2015-04-28 03:49:43 +02:00
{
return item.block.gasUsed;
})
.value();
return gasSpending;
}
2015-04-28 13:04:39 +02:00
History.prototype.getAvgHashrate = function()
{
2015-04-29 07:49:43 +02:00
if( _.isEmpty(this._items) )
2015-04-28 13:04:39 +02:00
return 0;
2015-04-29 07:49:43 +02:00
var blocktimeHistory = _( this._items )
2019-02-18 21:41:10 +01:00
.sortBy( 'height', false )
2015-06-17 03:45:46 +02:00
// .filter(function (item)
// {
// return item.block.trusted;
// })
2015-05-05 13:19:33 +02:00
.slice(0, 64)
2015-04-29 07:49:43 +02:00
.map(function (item)
2015-04-28 13:04:39 +02:00
{
return item.block.time;
})
.value();
2015-04-29 11:39:51 +02:00
var avgBlocktime = (_.sum(blocktimeHistory) / blocktimeHistory.length)/1000;
2015-04-28 13:04:39 +02:00
2015-05-05 13:19:33 +02:00
return this.bestBlock().block.difficulty / avgBlocktime;
2015-04-28 13:04:39 +02:00
}
2015-04-29 11:00:01 +02:00
History.prototype.getMinersCount = function()
{
var miners = _( this._items )
2019-02-18 21:41:10 +01:00
.sortBy( 'height', false )
2015-06-17 03:45:46 +02:00
// .filter(function (item)
// {
// return item.block.trusted;
// })
2015-04-29 11:00:01 +02:00
.slice(0, MAX_BINS)
.map(function (item)
{
return item.block.miner;
})
.value();
var minerCount = [];
_.forEach( _.countBy(miners), function (cnt, miner)
{
minerCount.push({ miner: miner, name: false, blocks: cnt });
});
return _(minerCount)
2019-02-18 21:41:10 +01:00
.sortBy( 'blocks', false )
2015-08-06 12:54:10 +02:00
.slice(0, 2)
2015-04-29 11:00:01 +02:00
.value();
}
2015-06-09 01:40:59 +02:00
History.prototype.setCallback = function(callback)
2015-04-28 03:45:23 +02:00
{
2015-06-09 01:40:59 +02:00
this._callback = callback;
}
2015-04-28 03:45:23 +02:00
2015-06-09 01:40:59 +02:00
History.prototype.getCharts = function()
{
if(this._callback !== null)
{
var chartHistory = _( this._items )
2020-08-21 09:17:35 +02:00
.orderBy( 'height', "desc" )
2015-06-17 03:45:46 +02:00
// .filter(function (item)
// {
// return item.block.trusted;
// })
2015-06-09 01:40:59 +02:00
.slice(0, MAX_BINS)
.reverse()
.map(function (item)
{
return {
height: item.height,
blocktime: item.block.time / 1000,
difficulty: item.block.difficulty,
uncles: item.block.uncles.length,
transactions: item.block.transactions ? item.block.transactions.length : 0,
2015-06-09 01:40:59 +02:00
gasSpending: item.block.gasUsed,
2015-08-06 12:22:47 +02:00
gasLimit: item.block.gasLimit,
2015-06-09 01:40:59 +02:00
miner: item.block.miner
};
})
.value();
this._callback(null, {
2019-02-18 21:41:10 +01:00
height : _.map( chartHistory, 'height' ),
blocktime : _.map( chartHistory, 'blocktime' ),
// avgBlocktime : _.sum(_.map( chartHistory, 'blocktime' )) / (chartHistory.length === 0 ? 1 : chartHistory.length),
avgBlocktime : this.getAvgBlocktime(),
2019-02-18 21:41:10 +01:00
difficulty : _.map( chartHistory, 'difficulty' ),
uncles : _.map( chartHistory, 'uncles' ),
transactions : _.map( chartHistory, 'transactions' ),
gasSpending : _.map( chartHistory, 'gasSpending' ),
gasLimit : _.map( chartHistory, 'gasLimit' ),
2015-06-09 01:40:59 +02:00
miners : this.getMinersCount(),
propagation : this.getBlockPropagation(),
uncleCount : this.getUncleCount(),
avgHashrate : this.getAvgHashrate()
});
}
2015-04-28 09:43:56 +02:00
}
History.prototype.requiresUpdate = function()
{
2016-12-11 17:42:37 +01:00
// return ( this._items.length < MAX_HISTORY && !_.isEmpty(this._items) );
return ( this._items.length < MAX_HISTORY );
2015-04-28 09:43:56 +02:00
}
2015-04-29 07:49:43 +02:00
History.prototype.getHistoryRequestRange = function()
2015-04-28 09:43:56 +02:00
{
2015-10-28 22:23:17 +01:00
if( this._items.length < 2 )
2015-04-29 07:49:43 +02:00
return false;
2019-02-18 21:41:10 +01:00
var blocks = _.map( this._items, 'height' );
2015-04-29 07:49:43 +02:00
var best = _.max( blocks );
var range = _.range( _.max([ 0, best - MAX_HISTORY ]), best + 1);
var missing = _.difference( range, blocks );
2015-04-29 07:58:11 +02:00
var max = _.max(missing);
2015-04-29 07:49:43 +02:00
var min = max - Math.min( 50, (MAX_HISTORY - this._items.length + 1) ) + 1;
2015-04-28 09:43:56 +02:00
2015-04-29 11:00:01 +02:00
return {
max: max,
min: min,
list: _( missing ).reverse().slice(0, 50).reverse().value()
};
2015-04-17 11:10:20 +02:00
}
module.exports = History;