train
Version:
Train a fast (FIFO) queue with a rollback mechanism. Behind the scenes it uses 2 arrays to simulate and perform fast shifting and popping operations without using the Array#shift() method..
471 lines (437 loc) • 14.8 kB
JavaScript
/*
* Train, a fast (FIFO) queue with rollback mechanism.
* Behind the scenes, it uses 2 arrays, to simulate and perform
* fast shifting and popping operations, without using the Array
* #shift() method.
*
* Copyright(c) 2012-present Guglielmo Ferri <44gatti@gmail.com>
* MIT Licensed
*/
exports.Train = ( function () {
var emptyFn = function () {}
, Bolgia = require( 'bolgia' )
, improve = Bolgia.improve
, clone = Bolgia.clone
, isArray = Array.isArray
, aPush = Array.prototype.push
, abs = Math.abs
, max = Math.max
, min = Math.min
// Train default options
, train_opt = {
head : null
, xlim : Infinity
, rlim : Infinity
}
// Train
, Train = function ( opt ) {
var me = this
, is = me instanceof Train
, cfg = null
;
if ( ! is ) return new Train( opt );
cfg = improve( clone( opt ), train_opt );
/*
* Private properties used to simulate a single queue are:
* 2 arrays qhead and qtail, an index hpos that points to
* the current head element.
*/
me.qhead = isArray( cfg.head ) ? cfg.head : [];
me.qtail = [];
// rollback array
me.qroll = [];
me.rpos = 0;
me.roll = false;
// current head index
me.hpos = 0;
// current index of iterator
me.ipos = 0;
/*
* Set a max capacity for the queue, when this limit
* will be reached, items pushed with xpush() will
* be not added to the queue, but silently ignored.
*/
me.xlim = cfg.xlim && typeof cfg.xlim === 'number' ? abs( cfg.xlim ) : Infinity;
// set rollback limit, equal to xlim for default ( a number or infinity )
me.rlim = cfg.rlim && typeof cfg.rlim === 'number' ? abs( cfg.rlim ) : cfg.xlim;
}
, tproto = Train.prototype
;
tproto.indexOf = function ( el, from ) {
var me = this
, o = el
, f = +from || 0
, qhead = me.qhead
, qtail = me.qtail
, hpos = me.hpos
, hlen = qhead.length - hpos
, offset = f - hlen
, r = -1
;
if ( offset < 0 ) {
r = qhead.indexOf( o, f + hpos );
if ( ~ r ) {
return r - hpos;
}
offset = 0;
}
r = qtail.indexOf( o, offset );
return ~ r ? r + hlen : -1;
};
tproto.rollUp = function ( bool ) {
var me = this
;
// enable roll
me.roll = bool === undefined ? true : bool;
// reset roll queue
me.qroll.length = 0;
// set roll index to the current head position
me.rpos = me.roll ? me.hpos : 0;
return me;
};
tproto.rollBack = function ( bool ) {
var me = this
, qroll = me.qroll
, qhead = me.qhead
;
if ( ! me.roll ) return me;
// disable or re-enable roll
me.roll = bool === undefined ? false : bool;
// concat roll with head array
me.qhead = qroll.concat( qhead );
// reset
me.hpos = me.rpos = 0;
me.qroll = [];
// update iterator position
me.ipos += qroll.length;
return me;
};
tproto.shift = function () {
var me = this
, qhead = me.qhead
, qroll = me.qroll
, rlim = me.rlim
, rsize = -1
, offset = -1
, buildRollBackQueue = function () {
rsize = qhead.length - me.rpos;
me.qroll = qhead.slice( rsize > rlim ? qhead.length - rlim : me.rpos );
me.hpos = me.rpos = 0;
me.qhead = me.qtail;
me.qtail = [];
return me.qhead.length ? me.qhead[ me.hpos++ ] : undefined;
}
, updateRollBackQueue = function () {
// roll position rpos should be 0
rsize = qhead.length;
if ( rsize > rlim ) {
// remove old qroll and replace with sliced qhead
me.qroll = qhead.slice( rsize - rlim );
} else {
offset = rsize + qroll.length - rlim;
if ( offset > 0 ) {
// evict some qroll elements
me.qroll = me.qroll.slice( offset );
aPush.apply( me.qroll, qhead );
} else {
// push all elements to qroll
aPush.apply( me.qroll, qhead );
}
}
}
;
// decrement iterator current position
if ( me.ipos > 0 ) --me.ipos;
// head array is empty or it is last index position
if ( me.hpos === qhead.length ) {
if ( me.roll ) {
if ( ! qroll.length ) return buildRollBackQueue();
updateRollBackQueue();
}
// flush qhead array and reset positions
me.hpos = me.rpos = 0;
// swap head and tail
me.qhead = qhead = me.qtail;
me.qtail = [];
return qhead.length ? qhead[ me.hpos++ ] : undefined;
}
return qhead[ me.hpos++ ];
};
// evict multiple elements from queue
tproto.pop = function ( k ) {
var me = this
, l = k ? min( k, me.size() ) : 0
, qhead = me.qhead
, qtail = me.qtail
, qroll = me.qroll
, hpos = me.hpos
, hlen = qhead.length - hpos
, result = null
, updateRollBackQueue = function ( tail ) {
var rsize = -1
, rlim = me.rlim
, rpos = me.rpos
, temp = null
, offset = -1
;
rsize = qhead.length + tail.length;
// TODO NOTE: could be better, but more complex
temp = qhead.slice( rpos ).concat( tail );
if ( ! qroll.length ) {
rsize -= rpos;
me.qroll = rsize > rlim ? temp.slice( rsize - rlim ) : temp;
me.rpos = 0;
return;
}
// roll position rpos should be 0
me.rpos = 0;
// if rsize is > roll limit, replace qroll with sliced temp
if ( rsize > rlim ) return me.qroll = temp.slice( rsize - rlim );
// calculate offset
offset = rsize + qroll.length - rlim - 1;
// evict some qroll elements?
if ( offset > 0 ) me.qroll = me.qroll.slice( offset );
// push all elements to qroll
aPush.apply( me.qroll, temp );
}
;
if ( l < 2 ) return ( ( result = me.shift() ) !== undefined ) ? [ result ] : [];
// update the iterator position
me.ipos = max( 0, me.ipos - l );
// pop elements only from qhead?
if ( hlen >= l ) return qhead.slice( hpos, me.hpos = hpos + l );
// 0 <= hlen < l, if 0, then head array could be empty or the last index position
result = qtail.splice( 0, l - hlen );
// check roll queue
if ( me.roll ) updateRollBackQueue( result );
// reset
me.hpos = 0;
me.qhead = me.qtail;
me.qtail = [];
// if hlen > 0, then get items from both qhead and qtail
return hlen ? qhead.slice( hpos ).concat( result ) : result;
};
tproto.slice = function ( begin, end ) {
var me = this
, qtail = me.qtail
, qhead = me.qhead
, size = me.size()
, pos = me.hpos
, hlen = qhead.length - pos
, b = Number( begin )
, e = Number( end )
, offb = 0
, offe = 0
;
if ( isNaN( b ) ) {
b = 0;
} else if( b < 0 ) {
b = max( 0, size + b );
}
if ( isNaN( e ) ) {
e = size;
} else if ( e < 0 ) {
e = max( 0, size + e );
}
if ( b > e ) {
return [];
}
offb = b - hlen;
offe = e - hlen;
if ( offb > 0 ) {
// use only qtail
return qtail.slice( offb, offe );
} else {
if ( offe > 0 ) {
// both queues
return qhead.slice( b ).concat( qtail.slice( 0, offe ) );
} else {
// use only qhead
return qhead.slice( b, e );
}
}
};
// NOTE: accessors are very slow, use the size() method
Object.defineProperty( tproto, 'length', {
get : function () {
var me = this;
return me.qhead.length - me.hpos + me.qtail.length;
}
} );
tproto.size = function () {
var me = this;
return me.qhead.length - me.hpos + me.qtail.length;
};
tproto.concat = function ( o ) {
var me = this
, arr = isArray( o ) ? o : o ? [ o ] : []
, qtail = me.qtail
;
aPush.apply( qtail, arr );
return me;
};
tproto.xconcat = function ( o ) {
var me = this
, arr = isArray( o ) ? o : o ? [ o ] : []
, alen = arr.length
, xsize = me.size() + alen
, qtail = me.qtail
;
return ( xsize <= me.xlim ) ? aPush.apply( qtail, o ) : -1;
};
tproto.melt = function ( tlist, all ) {
var me = this
, lt = isArray( tlist ) ? tlist : []
, llen = tlist.length
, i = 0
, t = lt[ 0 ]
, a = null
;
for ( ; i < llen; t = lt[ ++i ] ) {
if ( t instanceof Train ) {
a = t.pop( Infinity );
if ( a ) me.concat( a );
} else if ( isArray( t ) ) {
me.concat( t );
} else if ( all ) {
me.push( t );
}
}
return me.size();
};
tproto.push = function () {
var me = this
, qtail = me.qtail
, k = aPush.apply( qtail, arguments )
;
return k + me.qhead.length - me.hpos;
};
// faster push of a single element, not using apply.
tproto.fpush = function ( o ) {
var me = this
;
return me.qtail.push( o ) + me.qhead.length - me.hpos;
};
tproto.xpush = function () {
var me = this
, args = arguments
, size = me.size()
, xsize = args.length + size
, qtail = me.qtail
;
return xsize <= me.xlim ?
aPush.apply( qtail, args ) + me.qhead.length - me.hpos :
-1
;
};
tproto.flush = function ( evict ) {
var me = this
, len = me.size()
, ev = evict === undefined ? true : evict
;
if ( ! ev ) {
return 0;
}
me.qhead.length = me.qtail.length = me.qroll.length = 0;
me.hpos = me.rpos = me.ipos = 0;
// disable rollback
me.roll = false;
return len;
};
tproto.get = function ( index ) {
var me = this
, hlen = me.qhead.length
, i = index || 0
, j = me.hpos + i
, offset = j - hlen
;
if ( me.hpos === hlen ) {
return me.qtail[ i ];
}
if ( offset >= 0 ) {
return me.qtail[ offset ];
}
return me.qhead[ j ];
};
// circular get
tproto.cget = function ( i ) {
var me = this
, size = me.size() || Infinity
;
return me.get( i % size );
};
/*
* Get the current element through a circular iterator,
* incrementing internal counter/position by one; optionally,
* it is possible to specify the next iterator position/index
* with a number.
*/
tproto.next = function ( j ) {
var me = this
, cpos = me.ipos
, size = me.size() || Infinity
;
// set or increment iterator position
me.ipos = ( j >= 0 ? j : cpos + 1 ) % size;
return me.get( cpos );
};
// return current element through the circular iterator
tproto.curr = function () {
var me = this;
return me.get( me.ipos );
};
tproto.forEach = function ( fn, scope ) {
var me = this
/*
* Note : size is fixed to the current queue size,
* then it is possible to push other elements to the queue,
* these added elements are not affected by iteration.
*/
, qsize = me.size()
, cnt = 0
, env = scope || me
, nope = ( typeof fn !== 'function' )
, emsg = 'Train#forEach: first arg should be a function!'
;
if ( nope ) throw new TypeError( emsg );
if ( ! qsize ) return me;
while ( cnt < qsize ) {
// fn gets 3 args: Object element, Number index, Number qsize
fn.call( env, me.get( cnt ), cnt, qsize );
++cnt;
}
return me;
};
tproto.iterate = function ( fn, scope, cback, evict ) {
var me = this
, cnt = 0
, env = scope || me
, size = me.size()
, done = typeof cback === 'function' ? cback : emptyFn
, nope = typeof fn !== 'function'
, emsg = 'Train#iterate: first arg should be a function!'
;
if ( nope ) {
done( new TypeError( emsg ) );
return;
}
if ( size ) {
me.forEach( function ( el, index, qsize ) {
fn.call( env, el, index, function ( err ) {
if ( err ) {
done( err, index );
// reset fn done()
done = emptyFn;
return;
}
if ( ++cnt === qsize ) {
me.flush( evict === undefined ? false : true );
done( null, qsize );
}
} );
}, env );
} else done( null, 0 );
return me;
};
return Train;
} )();