diff --git a/lib/http/rpc.js b/lib/http/rpc.js index 9bfb1983..e1b5d457 100644 --- a/lib/http/rpc.js +++ b/lib/http/rpc.js @@ -327,7 +327,7 @@ RPC.prototype.getinfo = co(function* getinfo(args) { balance: Amount.btc(balance.unconfirmed, true), blocks: this.chain.height, timeoffset: this.network.time.offset, - connections: this.pool.peers.all.length, + connections: this.pool.peers.size(), proxy: '', difficulty: this._getDifficulty(), testnet: this.network.type !== Network.main, @@ -379,7 +379,7 @@ RPC.prototype.getnetworkinfo = co(function* getnetworkinfo(args) { protocolversion: constants.VERSION, localservices: this.pool.services, timeoffset: this.network.time.offset, - connections: this.pool.peers.all.length, + connections: this.pool.peers.size(), networks: [], relayfee: Amount.btc(this.network.getMinRelay(), true), localaddresses: [], @@ -413,7 +413,7 @@ RPC.prototype.addnode = co(function* addnode(args) { case 'onetry': if (!this.pool.peers.get(addr)) { peer = this.pool.createPeer(addr); - this.pool.peers.addPending(peer); + this.pool.peers.addOutbound(peer); } break; } @@ -450,47 +450,46 @@ RPC.prototype.getaddednodeinfo = co(function* getaddednodeinfo(args) { peer = this.pool.peers.get(addr); if (!peer) throw new RPCError('Node has not been added.'); - peers = [peer]; - } else { - peers = this.pool.peers.all; + return [this._toAddedNode(peer)]; } - for (i = 0; i < peers.length; i++) { - peer = peers[i]; - out.push({ - addednode: peer.hostname, - connected: peer.connected, - addresses: [ - { - address: peer.hostname, - connected: peer.outbound - ? 'outbound' - : 'inbound' - } - ] - }); - } + for (peer = this.pool.peers.head(); peer; peer = peer.next) + out.push(this._toAddedNode(peer)); return out; }); +RPC.prototype._toAddedNode = function _toAddedNode(peer) { + return { + addednode: peer.hostname, + connected: peer.connected, + addresses: [ + { + address: peer.hostname, + connected: peer.outbound + ? 'outbound' + : 'inbound' + } + ] + }; +}; + RPC.prototype.getconnectioncount = co(function* getconnectioncount(args) { if (args.help || args.length !== 0) throw new RPCError('getconnectioncount'); - return this.pool.peers.all.length; + return this.pool.peers.size(); }); RPC.prototype.getnettotals = co(function* getnettotals(args) { var sent = 0; var recv = 0; - var i, peer; + var peer; if (args.help || args.length > 0) throw new RPCError('getnettotals'); - for (i = 0; i < this.pool.peers.all.length; i++) { - peer = this.pool.peers.all[i]; + for (peer = this.pool.peers.head(); peer; peer = peer.next) { sent += peer.socket.bytesWritten; recv += peer.socket.bytesRead; } @@ -504,13 +503,12 @@ RPC.prototype.getnettotals = co(function* getnettotals(args) { RPC.prototype.getpeerinfo = co(function* getpeerinfo(args) { var peers = []; - var i, peer; + var peer; if (args.help || args.length !== 0) throw new RPCError('getpeerinfo'); - for (i = 0; i < this.pool.peers.all.length; i++) { - peer = this.pool.peers.all[i]; + for (peer = this.pool.peers.head(); peer; peer = peer.next) { peers.push({ id: peer.id, addr: peer.hostname, @@ -538,13 +536,11 @@ RPC.prototype.getpeerinfo = co(function* getpeerinfo(args) { }); RPC.prototype.ping = co(function* ping(args) { - var i; - if (args.help || args.length !== 0) throw new RPCError('ping'); - for (i = 0; i < this.pool.peers.all.length; i++) - this.pool.peers.all[i].sendPing(); + for (peer = this.pool.peers.head(); peer; peer = peer.next) + peer.sendPing(); return null; }); @@ -1538,7 +1534,7 @@ RPC.prototype.getblocktemplate = co(function* getblocktemplate(args) { } if (!this.network.selfConnect) { - if (this.pool.peers.all.length === 0) + if (this.pool.peers.size() === 0) throw new RPCError('Bitcoin is not connected!'); if (!this.chain.isFull()) diff --git a/lib/http/server.js b/lib/http/server.js index e27f9478..5f401095 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -631,7 +631,6 @@ HTTPServer.prototype._init = function _init() { this.get('/', function(req, res, send, next) { var totalTX = this.mempool ? this.mempool.totalTX : 0; var size = this.mempool ? this.mempool.getSize() : 0; - var loader = this.pool.peers.load ? 1 : 0; send(200, { version: constants.USER_VERSION, @@ -644,9 +643,9 @@ HTTPServer.prototype._init = function _init() { }, pool: { services: this.pool.services.toString(2), - outbound: this.pool.peers.outbound.length + loader, - pending: this.pool.peers.pending.length, - inbound: this.pool.peers.inbound.length + outbound: this.pool.peers.outbound, + pending: this.pool.peers.pending, + inbound: this.pool.peers.inbound }, mempool: { tx: totalTX, diff --git a/lib/net/peer.js b/lib/net/peer.js index 504a8e68..07616b9b 100644 --- a/lib/net/peer.js +++ b/lib/net/peer.js @@ -26,6 +26,7 @@ var BIP152 = require('./bip152'); var Block = require('../primitives/block'); var TX = require('../primitives/tx'); var errors = require('../btc/errors'); +var List = require('../utils/list'); var packetTypes = packets.types; var VerifyResult = errors.VerifyResult; @@ -93,6 +94,7 @@ function Peer(pool, addr, socket) { this.version = null; this.destroyed = false; this.ack = false; + this.pending = true; this.connected = false; this.ts = 0; this.preferHeaders = false; @@ -119,6 +121,9 @@ function Peer(pool, addr, socket) { this.drainSize = 0; this.drainQueue = []; + this.next = null; + this.prev = null; + this.challenge = null; this.lastPong = -1; this.lastPing = -1; @@ -134,8 +139,8 @@ function Peer(pool, addr, socket) { this.requestTimeout = 10000; this.requestMap = {}; - this.queueBlock = []; - this.queueTX = []; + this.queueBlock = new List(); + this.queueTX = new List(); this.uid = 0; this.id = Peer.uid++; @@ -154,6 +159,7 @@ function Peer(pool, addr, socket) { } else { this.socket = socket; this.connected = true; + this.pending = false; } if (this.options.bip151) { @@ -491,7 +497,7 @@ Peer.prototype._finalize = co(function* _finalize() { yield this.updateWatch(); // Announce our currently broadcasted items. - yield this.announce(this.pool.invItems); + yield this.announce(this.pool.invItems.toArray()); // Set a fee rate filter. if (this.pool.feeRate !== -1) @@ -1883,7 +1889,7 @@ Peer.prototype._handleAddr = function _handleAddr(packet) { 'Received %d addrs (hosts=%d, peers=%d) (%s).', addrs.length, this.pool.hosts.items.length, - this.pool.peers.all.length, + this.pool.peers.size(), this.hostname); this.fire('addr', addrs); diff --git a/lib/net/pool.js b/lib/net/pool.js index 46b92240..2c37f87b 100644 --- a/lib/net/pool.js +++ b/lib/net/pool.js @@ -29,6 +29,7 @@ var tcp = require('./tcp'); var request = require('../http/request'); var VerifyError = errors.VerifyError; var VerifyResult = errors.VerifyResult; +var List = require('../utils/list'); /** * A pool of peers for handling all network activity. @@ -145,7 +146,7 @@ function Pool(options) { // Currently broadcasted objects. this.invMap = {}; - this.invItems = []; + this.invItems = new List(); this.invTimeout = 60000; this.scheduled = false; @@ -327,14 +328,14 @@ Pool.prototype._open = co(function* _open() { */ Pool.prototype._close = co(function* close() { - var i, items, hashes, hash; + var i, next, item, hashes, hash; this.stopSync(); - items = this.invItems.slice(); - - for (i = 0; i < items.length; i++) - items[i].finish(); + for (item = this.invItems.head; item; item = next) { + next = item.next; + item.finish(); + } hashes = Object.keys(this.requestMap); @@ -467,7 +468,7 @@ Pool.prototype._handleLeech = function _handleLeech(socket) { addr = NetworkAddress.fromSocket(socket, this.network); - if (this.peers.inbound.length >= this.maxInbound) { + if (this.peers.inbound >= this.maxInbound) { this.logger.debug('Ignoring leech: too many inbound (%s).', addr.hostname); socket.destroy(); return; @@ -613,7 +614,9 @@ Pool.prototype.addLoader = function addLoader() { this.logger.info('Added loader peer (%s).', peer.hostname); - this.peers.addLoader(peer); + assert(!this.peers.load); + this.peers.load = peer; + this.peers.add(peer); this.fillPeers(); util.nextTick(function() { @@ -675,13 +678,13 @@ Pool.prototype.startSync = function startSync() { */ Pool.prototype.sync = function sync() { - var i; + var peer; - if (this.peers.load) - this.peers.load.trySync(); - - for (i = 0; i < this.peers.outbound.length; i++) - this.peers.outbound[i].trySync(); + for (peer = this.peers.head(); peer; peer = peer.next) { + if (!peer.outbound || peer.pending) + continue; + peer.trySync(); + } }; /** @@ -690,15 +693,11 @@ Pool.prototype.sync = function sync() { */ Pool.prototype.forceSync = function forceSync() { - var i, peer; + var peer; - if (this.peers.load) { - this.peers.load.syncSent = false; - this.peers.load.trySync(); - } - - for (i = 0; i < this.peers.outbound.length; i++) { - peer = this.peers.outbound[i]; + for (peer = this.peers.head(); peer; peer = peer.next) { + if (!peer.outbound || peer.pending) + continue; peer.syncSent = false; peer.trySync(); } @@ -709,7 +708,7 @@ Pool.prototype.forceSync = function forceSync() { */ Pool.prototype.stopSync = function stopSync() { - var i; + var peer; if (!this.syncing) return; @@ -722,11 +721,11 @@ Pool.prototype.stopSync = function stopSync() { this.stopInterval(); this.stopTimeout(); - if (this.peers.load) - this.peers.load.syncSent = false; - - for (i = 0; i < this.peers.outbound.length; i++) - this.peers.outbound[i].syncSent = false; + for (peer = this.peers.head(); peer; peer = peer.next) { + if (!peer.outbound || peer.pending) + continue; + peer.syncSent = false; + } }; /** @@ -991,9 +990,9 @@ Pool.prototype._handleBlock = co(function* _handleBlock(block, peer) { this.chain.total, this.chain.orphan.count, this.activeBlocks, - peer.queueBlock.length, + peer.queueBlock.size, block.bits, - this.peers.all.length, + this.peers.size(), this.chain.locker.pending.length, this.chain.locker.jobs.length); } @@ -1011,13 +1010,13 @@ Pool.prototype._handleBlock = co(function* _handleBlock(block, peer) { */ Pool.prototype.sendMempool = function sendMempool() { - var i; + var peer; - if (this.peers.load) - this.peers.load.sendMempool(); - - for (i = 0; i < this.peers.outbound.length; i++) - this.peers.outbound[i].sendMempool(); + for (peer = this.peers.head(); peer; peer = peer.next) { + if (!peer.outbound || peer.pending) + continue; + peer.sendMempool(); + } }; /** @@ -1026,16 +1025,10 @@ Pool.prototype.sendMempool = function sendMempool() { */ Pool.prototype.sendAlert = function sendAlert(alert) { - var i; + var peer; - if (this.peers.load) - this.peers.load.sendAlert(alert); - - for (i = 0; i < this.peers.outbound.length; i++) - this.peers.outbound[i].sendAlert(alert); - - for (i = 0; i < this.peers.inbound.length; i++) - this.peers.inbound[i].sendAlert(alert); + for (peer = this.peers.head(); peer; peer = peer.next) + peer.sendAlert(alert); }; /** @@ -1077,7 +1070,7 @@ Pool.prototype.createPeer = function createPeer(addr, socket) { self.stopInterval(); self.stopTimeout(); - if (self.peers.size() === 0) { + if (self.peers.outbound === 0) { self.logger.warning('%s %s %s', 'Could not connect to any peers.', 'Do you have a network connection?', @@ -1415,7 +1408,7 @@ Pool.prototype.addLeech = function addLeech(addr, socket) { this.logger.info('Added leech peer (%s).', peer.hostname); - this.peers.addLeech(peer); + this.peers.add(peer); util.nextTick(function() { self.emit('leech', peer); @@ -1435,7 +1428,7 @@ Pool.prototype.addPeer = function addPeer() { if (!this.loaded) return; - if (this.peers.isFull()) + if (this.peers.outbound >= this.maxOutbound) return; // Hang back if we don't have a loader peer yet. @@ -1449,7 +1442,7 @@ Pool.prototype.addPeer = function addPeer() { peer = this.createPeer(addr); - this.peers.addPending(peer); + this.peers.add(peer); util.nextTick(function() { self.emit('peer', peer); @@ -1465,7 +1458,7 @@ Pool.prototype.fillPeers = function fillPeers() { var i; this.logger.debug('Refilling peers (%d/%d).', - this.peers.all.length - this.peers.inbound.length, + this.peers.outbound, this.maxOutbound); for (i = 0; i < this.maxOutbound - 1; i++) @@ -1541,19 +1534,18 @@ Pool.prototype.unwatch = function unwatch() { Pool.prototype.updateWatch = function updateWatch() { var self = this; - var i; + var peer; if (this.pendingWatch != null) return; this.pendingWatch = setTimeout(function() { self.pendingWatch = null; - - if (self.peers.load) - self.peers.load.updateWatch(); - - for (i = 0; i < self.peers.outbound.length; i++) - self.peers.outbound[i].updateWatch(); + for (peer = self.peers.head(); peer; peer = peer.next) { + if (!peer.outbound || peer.pending) + continue; + peer.updateWatch(); + } }, 50); }; @@ -1634,16 +1626,15 @@ Pool.prototype.getTX = function getTX(peer, hash) { item = new LoadRequest(this, peer, this.txType, hash); - if (peer.queueTX.length === 0) { + if (peer.queueTX.size === 0) { util.nextTick(function() { self.logger.debug( 'Requesting %d/%d txs from peer with getdata (%s).', - peer.queueTX.length, + peer.queueTX.size, self.activeTX, peer.hostname); - peer.getData(peer.queueTX); - peer.queueTX.length = 0; + peer.getData(peer.queueTX.slice()); }); } @@ -1709,29 +1700,29 @@ Pool.prototype.scheduleRequests = co(function* scheduleRequests(peer) { */ Pool.prototype.sendRequests = function sendRequests(peer) { - var i, size, items; + var i, size, items, item; - if (peer.queueBlock.length === 0) + if (peer.queueBlock.size === 0) return; if (this.options.spv) { - if (this.activeBlocks >= 500) + if (this.activeBlocks >= 2000) return; - items = peer.queueBlock.slice(); - peer.queueBlock.length = 0; + size = peer.queueBlock.size; } else { size = this.network.getBatchSize(this.chain.height); if (this.activeBlocks >= size) return; - - items = peer.queueBlock.slice(0, size); - peer.queueBlock = peer.queueBlock.slice(size); } - for (i = 0; i < items.length; i++) - items[i] = items[i].start(); + items = peer.queueBlock.slice(size); + + for (i = 0; i < items.length; i++) { + item = items[i]; + item.start(); + } this.logger.debug( 'Requesting %d/%d blocks from peer with getdata (%s).', @@ -1799,13 +1790,13 @@ Pool.prototype.broadcast = function broadcast(msg) { */ Pool.prototype.announce = function announce(msg) { - var i; + var peer; - if (this.peers.load) - this.peers.load.tryAnnounce(msg); - - for (i = 0; i < this.peers.outbound.length; i++) - this.peers.outbound[i].tryAnnounce(msg); + for (peer = this.peers.head(); peer; peer = peer.next) { + if (!peer.outbound || peer.pending) + continue; + peer.tryAnnounce(msg); + } }; /** @@ -1814,15 +1805,15 @@ Pool.prototype.announce = function announce(msg) { */ Pool.prototype.setFeeRate = function setFeeRate(rate) { - var i; + var peer; this.feeRate = rate; - if (this.peers.load) - this.peers.load.sendFeeRate(rate); - - for (i = 0; i < this.peers.outbound.length; i++) - this.peers.outbound[i].sendFeeRate(rate); + for (peer = this.peers.head(); peer; peer = peer.next) { + if (!peer.outbound || peer.pending) + continue; + peer.sendFeeRate(rate); + } }; /** @@ -1978,52 +1969,48 @@ Pool.prototype.getIP2 = co(function* getIP2() { function PeerList(pool) { this.pool = pool; - // Peers that are loading blocks themselves - this.outbound = []; - // Peers that are still connecting - this.pending = []; - // Peers that connected to us - this.inbound = []; - // Peers that are loading block ids - this.load = null; - // All peers - this.all = []; - // Map of hostnames this.map = {}; + this.list = new List(); + this.load = null; + this.inbound = 0; + this.outbound = 0; + this.pending = 0; } -PeerList.prototype.addLoader = function addLoader(peer) { - this.load = peer; - this.all.push(peer); - assert(!this.map[peer.hostname]); - this.map[peer.hostname] = peer; +PeerList.prototype.head = function head() { + return this.list.head; }; -PeerList.prototype.addPending = function addPending(peer) { - this.pending.push(peer); - this.all.push(peer); - assert(!this.map[peer.hostname]); - this.map[peer.hostname] = peer; +PeerList.prototype.tail = function tail() { + return this.list.tail; }; -PeerList.prototype.addLeech = function addLeech(peer) { - this.inbound.push(peer); - this.all.push(peer); - assert(!this.map[peer.hostname]); - this.map[peer.hostname] = peer; +PeerList.prototype.size = function size() { + return this.list.size; }; PeerList.prototype.promote = function promote(peer) { - if (util.binaryRemove(this.pending, peer, compare)) - util.binaryInsert(this.outbound, peer, compare); + assert(peer.outbound); + assert(peer.pending); + peer.pending = false; + this.pending--; +}; + +PeerList.prototype.add = function add(peer) { + assert(this.list.push(peer)); + assert(!this.map[peer.hostname]); + this.map[peer.hostname] = peer; + if (peer.outbound) { + this.outbound++; + if (peer.pending) + this.pending++; + } else { + this.inbound++; + } }; PeerList.prototype.remove = function remove(peer) { - util.binaryRemove(this.pending, peer, compare); - util.binaryRemove(this.outbound, peer, compare); - util.binaryRemove(this.inbound, peer, compare); - util.binaryRemove(this.all, peer, compare); - + assert(this.list.remove(peer)); assert(this.map[peer.hostname]); delete this.map[peer.hostname]; @@ -2031,66 +2018,38 @@ PeerList.prototype.remove = function remove(peer) { this.pool.logger.info('Removed loader peer (%s).', peer.hostname); this.load = null; } -}; -PeerList.prototype.demoteLoader = function demoteLoader() { - var peer = this.load; - assert(peer); - this.load = null; - if (peer.ack) - util.binaryInsert(this.outbound, peer, compare); - else - util.binaryInsert(this.pending, peer, compare); + if (peer.outbound) { + this.outbound--; + if (peer.pending) + this.pending--; + } else { + this.inbound--; + } }; PeerList.prototype.repurpose = function repurpose(peer) { - var r1, r2; - assert(peer.outbound); - - if (this.load) - this.demoteLoader(); - - r1 = util.binaryRemove(this.pending, peer, compare); - r2 = util.binaryRemove(this.outbound, peer, compare); - - assert(r1 || r2); - this.load = peer; }; -PeerList.prototype.isFull = function isFull() { - return this.size() >= this.pool.maxOutbound - 1; -}; - -PeerList.prototype.size = function size() { - return this.outbound.length + this.pending.length; -}; - PeerList.prototype.get = function get(addr) { return this.map[addr.hostname]; }; PeerList.prototype.destroy = function destroy() { - var i, peers; + var peer, next; - if (this.load) - this.load.destroy(); + this.map = {}; + this.load = null; + this.inbound = 0; + this.outbound = 0; + this.pending = 0; - peers = this.outbound.slice(); - - for (i = 0; i < peers.length; i++) - peers[i].destroy(); - - peers = this.pending.slice(); - - for (i = 0; i < peers.length; i++) - peers[i].destroy(); - - peers = this.inbound.slice(); - - for (i = 0; i < peers.length; i++) - peers[i].destroy(); + for (peer = this.list.head; peer; peer = next) { + next = peer.next; + peer.destroy(); + } }; /** @@ -2341,10 +2300,12 @@ function LoadRequest(pool, peer, type, hash) { this.type = type; this.hash = hash; this.active = false; - this.id = this.pool.uid++; this.timeout = null; this.onTimeout = this._onTimeout.bind(this); + this.next = null; + this.prev = null; + assert(!this.pool.requestMap[this.hash]); this.pool.requestMap[this.hash] = this; } @@ -2408,9 +2369,9 @@ LoadRequest.prototype.finish = function finish() { } if (this.type === this.pool.txType) - util.binaryRemove(this.peer.queueTX, this, compare); + this.peer.queueTX.remove(this); else - util.binaryRemove(this.peer.queueBlock, this, compare); + this.peer.queueBlock.remove(this); if (this.timeout != null) { clearTimeout(this.timeout); @@ -2502,7 +2463,7 @@ BroadcastItem.prototype.start = function start() { assert(!this.pool.invMap[this.hash], 'Already started.'); this.pool.invMap[this.hash] = this; - util.binaryInsert(this.pool.invItems, this, compare); + assert(this.pool.invItems.push(this)); this.refresh(); @@ -2550,7 +2511,7 @@ BroadcastItem.prototype.finish = function finish(err) { this.timeout = null; delete this.pool.invMap[this.hash]; - util.binaryRemove(this.pool.invItems, this, compare); + assert(this.pool.invItems.remove(this)); for (i = 0; i < this.callback.length; i++) this.callback[i](err); diff --git a/lib/utils/list.js b/lib/utils/list.js new file mode 100644 index 00000000..7b9af0f5 --- /dev/null +++ b/lib/utils/list.js @@ -0,0 +1,237 @@ +'use strict'; + +var assert = require('assert'); + +/** + * A linked list. + * @exports List + * @constructor + */ + +function List() { + if (!(this instanceof List)) + return new List(); + + this.head = null; + this.tail = null; + this.size = 0; +} + +/** + * Reset the cache. Clear all items. + */ + +List.prototype.reset = function reset() { + var item, next; + + for (item = this.head; item; item = next) { + next = item.next; + item.prev = null; + item.next = null; + } + + assert(!item); + + this.head = null; + this.tail = null; + this.size = 0; +}; + +/** + * Remove the first item in the list. + */ + +List.prototype.shift = function shift() { + var item = this.head; + + if (!item) + return; + + this.remove(item); + + return item; +}; + +/** + * Prepend an item to the linked list (sets new head). + * @private + * @param {ListItem} + */ + +List.prototype.unshift = function unshift(item) { + return this.insert(null, item); +}; + +/** + * Append an item to the linked list (sets new tail). + * @private + * @param {ListItem} + */ + +List.prototype.push = function push(item) { + return this.insert(this.tail, item); +}; + +/** + * Remove the last item in the list. + */ + +List.prototype.pop = function pop() { + var item = this.tail; + + if (!item) + return; + + this.remove(item); + + return item; +}; + +/** + * Insert item into the linked list. + * @private + * @param {ListItem|null} ref + * @param {ListItem} item + */ + +List.prototype.insert = function insert(ref, item) { + if (item.prev || item.next || item === this.head) + return false; + + assert(!item.prev); + assert(!item.next); + + if (ref == null) { + if (!this.head) { + this.head = item; + this.tail = item; + } else { + this.head.prev = item; + item.next = this.head; + this.head = item; + } + this.size++; + return true; + } + + item.next = ref.next; + item.prev = ref; + ref.next = item; + + if (ref === this.tail) + this.tail = item; + + this.size++; + + return true; +}; + +/** + * Remove item from the linked list. + * @private + * @param {ListItem} + */ + +List.prototype.remove = function remove(item) { + if (!item.prev && !item.next && item !== this.head) + return false; + + if (item.prev) + item.prev.next = item.next; + + if (item.next) + item.next.prev = item.prev; + + if (item === this.head) + this.head = item.next; + + if (item === this.tail) + this.tail = item.prev || this.head; + + if (!this.head) + assert(!this.tail); + + if (!this.tail) + assert(!this.head); + + item.prev = null; + item.next = null; + + this.size--; + + return true; +}; + +/** + * Slice the list to an array of items. + * @param {Number} total + * @returns {Object[]} + */ + +List.prototype.slice = function slice(total) { + var items = []; + var item, next; + + if (total == null) + total = Infinity; + + for (item = this.head; item; item = next) { + next = item.next; + item.prev = null; + item.next = null; + + this.size--; + + items.push(item); + + if (items.length === total) + break; + } + + if (next) { + this.head = next; + next.prev = null; + } else { + this.head = null; + this.tail = null; + } + + return items; +}; + +/** + * Convert the list to an array of items. + * @returns {Object[]} + */ + +List.prototype.toArray = function toArray() { + var items = []; + var item; + + for (item = this.head; item; item = item.next) + items.push(item); + + return items; +}; + +/** + * Represents an LRU item. + * @constructor + * @private + * @param {String} key + * @param {Object} value + */ + +function ListItem(value) { + this.next = null; + this.prev = null; +} + +/* + * Expose + */ + +exports = List; +exports.Item = ListItem; + +module.exports = exports;