diff --git a/lib/Queue.js b/lib/Queue.js new file mode 100644 index 00000000..db980848 --- /dev/null +++ b/lib/Queue.js @@ -0,0 +1,124 @@ +/** + * A queue + * @param {Object} options + * Available options: + * - delay: number When a number, the queue will be flushed + * automatically after an inactivity of this delay + * in milliseconds. + * When false (default), the queue is not flushed + * - max: number When the queue exceeds the given maximum number + * of entries, the queue is flushed automatically. + * Default value of max is Infinity. + * @constructor + */ +function Queue(options) { + // options + this.delay = options && typeof options.delay === 'number' ? options.delay : false; + this.max = options && typeof options.max === 'number' ? options.max : Infinity; + + // properties + this._queue = []; + this._timeout = null; +} + +/** + * Extend an object with queuing functionality. + * The object will be extended with a function flush, and the methods provided + * in options.replace will be replaced with queued ones. + * @param {Object} object + * @param {Object} options + * Available options: + * - replace: Array. + * A list with method names of the methods + * on the object to be replaced with queued ones. + * - delay: number When a number, the queue will be flushed + * automatically after an inactivity of this delay + * in milliseconds. + * When false (default), the queue is not flushed + * - max: number When the queue exceeds the given maximum number + * of entries, the queue is flushed automatically. + * Default value of max is Infinity. + */ +Queue.extend = function (object, options) { + var queue = new Queue(options); + + if (object.flush !== undefined) { + throw new Error('Target object already has a property flush'); + } + object.flush = function () { + queue.flush(); + }; + + if (options && options.replace) { + for (var i = 0; i < options.replace.length; i++) { + queue.replace(object, options.replace[i]); + } + } +}; + +/** + * Replace a method on an object with a queued version + * @param {Object} object Object having the method + * @param {string} method The method name + */ +Queue.prototype.replace = function(object, method) { + var me = this; + var original = object[method]; + if (!original) { + throw new Error('Method ' + method + ' undefined'); + } + + object[method] = function () { + // create an Array with the arguments + var args = []; + for (var i = 0; i < arguments.length; i++) { + args[i] = arguments[i]; + } + + // add this call to the queue + me.queue({ + args: args, + fn: original, + context: this + }); + }; +}; + +/** + * Queue a call + * @param {function | {fn: function, args: Array} | {fn: function, args: Array, context: Object}} entry + */ +Queue.prototype.queue = function(entry) { + if (typeof entry === 'function') { + this._queue.push({fn: entry}); + } + else { + this._queue.push(entry); + } + + // flush when the maximum is exceeded. + if (this._queue.length > this.max) { + this.flush(); + } + + // flush after a period of inactivity when a delay is configured + if (typeof this.delay === 'number') { + var me = this; + clearTimeout(this._timeout); + this._timeout = setTimeout(function () { + me.flush(); + }, this.delay); + } +}; + +/** + * Flush all queued calls + */ +Queue.prototype.flush = function () { + while (this._queue.length > 0) { + var entry = this._queue.shift(); + entry.fn.apply(entry.context || entry.fn, entry.args || []); + } +}; + +module.exports = Queue; diff --git a/test/Queue.test.js b/test/Queue.test.js new file mode 100644 index 00000000..bef98b1f --- /dev/null +++ b/test/Queue.test.js @@ -0,0 +1,138 @@ +var assert = require('assert'); +var Queue = require('../lib/Queue'); + +describe('Queue', function () { + it('queue, queue actions', function () { + var queue = new Queue(); + + var count = 0; + function inc() { + count++; + } + + queue.queue(inc); + assert.equal(count, 0); + + queue.flush(); + assert.equal(count, 1); + }); + + it('queue, queue actions with a delay', function (done) { + var queue = new Queue({delay: 25}); + + var count = 0; + function inc() { + count++; + } + + queue.queue(inc); + assert.equal(count, 0); + + setTimeout(function () { + assert.equal(count, 1); + + done(); + }, 50); + }); + + it('queue, queue multiple actions with a delay', function (done) { + var queue = new Queue({delay: 100}); + + var count = 0; + function inc() { + count++; + } + + queue.queue(inc); + assert.equal(count, 0); + + setTimeout(function () { + queue.queue(inc); + assert.equal(count, 0); + + // flush should now occur after 100 ms from now, lets test after 75 and 125 ms + setTimeout(function () { + assert.equal(count, 0); + + setTimeout(function () { + assert.equal(count, 2); + + done(); + }, 50); + }, 75); + }, 50); + }); + + it('queue actions with args', function () { + var queue = new Queue(); + + var count = 0; + function add(value) { + count += value; + } + + queue.queue({fn: add, args: [2]}); + assert.equal(count, 0); + + queue.flush(); + assert.equal(count, 2); + }); + + it('queue actions with args and context', function () { + var queue = new Queue(); + + var obj = { + count: 0, + add: function (value) { + this.count += value; + } + }; + + queue.queue({context: obj, fn: obj.add, args: [2]}); + assert.equal(obj.count, 0); + + queue.flush(); + assert.equal(obj.count, 2); + }); + + it('replace functions on an object', function () { + var queue = new Queue(); + + var obj = { + count: 0, + add: function (value) { + this.count += value; + } + }; + + queue.replace(obj, 'add'); + + obj.add(3); + assert.equal(obj.count, 0); + + queue.flush(); + assert.equal(obj.count, 3); + }); + + it('extend an object', function () { + var obj = { + count: 0, + add: function (value) { + this.count += value; + }, + subtract: function (value) { + this.count -= value; + } + }; + + Queue.extend(obj, {replace: ['add', 'subtract']}); + + obj.add(3); + obj.subtract(1); + assert.equal(obj.count, 0); + + obj.flush(); + assert.equal(obj.count, 2); + }); + +});