/** * <p>AsyncQueue allows you create a chain of function callbacks executed * via setTimeout (or synchronously) that are guaranteed to run in order. * Items in the queue can be promoted or removed. Start or resume the * execution chain with run(). pause() to temporarily delay execution, or * stop() to halt and clear the queue.</p> * * @module async-queue */ /** * <p>A specialized queue class that supports scheduling callbacks to execute * sequentially, iteratively, even asynchronously.</p> * * <p>Callbacks can be function refs or objects with the following keys. Only * the <code>fn</code> key is required.</p> * * <ul> * <li><code>fn</code> -- The callback function</li> * <li><code>context</code> -- The execution context for the callbackFn.</li> * <li><code>args</code> -- Arguments to pass to callbackFn.</li> * <li><code>timeout</code> -- Millisecond delay before executing callbackFn. * (Applies to each iterative execution of callback)</li> * <li><code>iterations</code> -- Number of times to repeat the callback. * <li><code>until</code> -- Repeat the callback until this function returns * true. This setting trumps iterations.</li> * <li><code>autoContinue</code> -- Set to false to prevent the AsyncQueue from * executing the next callback in the Queue after * the callback completes.</li> * <li><code>id</code> -- Name that can be used to get, promote, get the * indexOf, or delete this callback.</li> * </ul> * * @class AsyncQueue * @extends EventTarget * @constructor * @param callback* {Function|Object} 0..n callbacks to seed the queue */ Y.AsyncQueue = function() { this._init(); this.add.apply(this, arguments); }; var Queue = Y.AsyncQueue, EXECUTE = 'execute', SHIFT = 'shift', PROMOTE = 'promote', REMOVE = 'remove', isObject = Y.Lang.isObject, isFunction = Y.Lang.isFunction; /** * <p>Static default values used to populate callback configuration properties. * Preconfigured defaults include:</p> * * <ul> * <li><code>autoContinue</code>: <code>true</code></li> * <li><code>iterations</code>: 1</li> * <li><code>timeout</code>: 10 (10ms between callbacks)</li> * <li><code>until</code>: (function to run until iterations <= 0)</li> * </ul> * * @property defaults * @type {Object} * @static */ Queue.defaults = Y.mix({ autoContinue : true, iterations : 1, timeout : 10, until : function () { this.iterations |= 0; return this.iterations <= 0; } }, Y.config.queueDefaults || {}); Y.extend(Queue, Y.EventTarget, { /** * Used to indicate the queue is currently executing a callback. * * @property _running * @type {Boolean|Object} true for synchronous callback execution, the * return handle from Y.later for async callbacks. * Otherwise false. * @protected */ _running : false, /** * Initializes the AsyncQueue instance properties and events. * * @method _init * @protected */ _init : function () { Y.EventTarget.call(this, { prefix: 'queue', emitFacade: true }); this._q = []; /** * Callback defaults for this instance. Static defaults that are not * overridden are also included. * * @property defaults * @type {Object} */ this.defaults = {}; this._initEvents(); }, /** * Initializes the instance events. * * @method _initEvents * @protected */ _initEvents : function () { this.publish({ 'execute' : { defaultFn : this._defExecFn, emitFacade: true }, 'shift' : { defaultFn : this._defShiftFn, emitFacade: true }, 'add' : { defaultFn : this._defAddFn, emitFacade: true }, 'promote' : { defaultFn : this._defPromoteFn, emitFacade: true }, 'remove' : { defaultFn : this._defRemoveFn, emitFacade: true } }); }, /** * Returns the next callback needing execution. If a callback is * configured to repeat via iterations or until, it will be returned until * the completion criteria is met. * * When the queue is empty, null is returned. * * @method next * @return {Function} the callback to execute */ next : function () { var callback; while (this._q.length) { callback = this._q[0] = this._prepare(this._q[0]); if (callback && callback.until()) { this.fire(SHIFT, { callback: callback }); callback = null; } else { break; } } return callback || null; }, /** * Default functionality for the "shift" event. Shifts the * callback stored in the event object's <em>callback</em> property from * the queue if it is the first item. * * @method _defShiftFn * @param e {Event} The event object * @protected */ _defShiftFn : function (e) { if (this.indexOf(e.callback) === 0) { this._q.shift(); } }, /** * Creates a wrapper function to execute the callback using the aggregated * configuration generated by combining the static AsyncQueue.defaults, the * instance defaults, and the specified callback settings. * * The wrapper function is decorated with the callback configuration as * properties for runtime modification. * * @method _prepare * @param callback {Object|Function} the raw callback * @return {Function} a decorated function wrapper to execute the callback * @protected */ _prepare: function (callback) { if (isFunction(callback) && callback._prepared) { return callback; } var config = Y.merge( Queue.defaults, { context : this, args: [], _prepared: true }, this.defaults, (isFunction(callback) ? { fn: callback } : callback)), wrapper = Y.bind(function () { if (!wrapper._running) { wrapper.iterations--; } if (isFunction(wrapper.fn)) { wrapper.fn.apply(wrapper.context || Y, Y.Array(wrapper.args)); } }, this); return Y.mix(wrapper, config); }, /** * Sets the queue in motion. All queued callbacks will be executed in * order unless pause() or stop() is called or if one of the callbacks is * configured with autoContinue: false. * * @method run * @return {AsyncQueue} the AsyncQueue instance * @chainable */ run : function () { var callback, cont = true; if (this._executing) { this._running = true; return this; } for (callback = this.next(); callback && !this.isRunning(); callback = this.next()) { cont = (callback.timeout < 0) ? this._execute(callback) : this._schedule(callback); // Break to avoid an extra call to next (final-expression of the // 'for' loop), because the until function of the next callback // in the queue may return a wrong result if it depends on the // not-yet-finished work of the previous callback. if (!cont) { break; } } if (!callback) { /** * Event fired when there is no remaining callback in the running queue. Also fired after stop(). * @event complete */ this.fire('complete'); } return this; }, /** * Handles the execution of callbacks. Returns a boolean indicating * whether it is appropriate to continue running. * * @method _execute * @param callback {Object} the callback object to execute * @return {Boolean} whether the run loop should continue * @protected */ _execute : function (callback) { this._running = callback._running = true; this._executing = callback; callback.iterations--; this.fire(EXECUTE, { callback: callback }); var cont = this._running && callback.autoContinue; this._running = callback._running = false; this._executing = false; return cont; }, /** * Schedules the execution of asynchronous callbacks. * * @method _schedule * @param callback {Object} the callback object to execute * @return {Boolean} whether the run loop should continue * @protected */ _schedule : function (callback) { this._running = Y.later(callback.timeout, this, function () { if (this._execute(callback)) { this.run(); } }); return false; }, /** * Determines if the queue is waiting for a callback to complete execution. * * @method isRunning * @return {Boolean} true if queue is waiting for a * from any initiated transactions */ isRunning : function () { return !!this._running; }, /** * Default functionality for the "execute" event. Executes the * callback function * * @method _defExecFn * @param e {Event} the event object * @protected */ _defExecFn : function (e) { e.callback(); }, /** * Add any number of callbacks to the end of the queue. Callbacks may be * provided as functions or objects. * * @method add * @param callback* {Function|Object} 0..n callbacks * @return {AsyncQueue} the AsyncQueue instance * @chainable */ add : function () { this.fire('add', { callbacks: Y.Array(arguments,0,true) }); return this; }, /** * Default functionality for the "add" event. Adds the callbacks * in the event facade to the queue. Callbacks successfully added to the * queue are present in the event's <code>added</code> property in the * after phase. * * @method _defAddFn * @param e {Event} the event object * @protected */ _defAddFn : function(e) { var _q = this._q, added = []; Y.Array.each(e.callbacks, function (c) { if (isObject(c)) { _q.push(c); added.push(c); } }); e.added = added; }, /** * Pause the execution of the queue after the execution of the current * callback completes. If called from code outside of a queued callback, * clears the timeout for the pending callback. Paused queue can be * restarted with q.run() * * @method pause * @return {AsyncQueue} the AsyncQueue instance * @chainable */ pause: function () { if (this._running && isObject(this._running)) { this._running.cancel(); } this._running = false; return this; }, /** * Stop and clear the queue after the current execution of the * current callback completes. * * @method stop * @return {AsyncQueue} the AsyncQueue instance * @chainable */ stop : function () { this._q = []; if (this._running && isObject(this._running)) { this._running.cancel(); this._running = false; } // otherwise don't systematically set this._running to false, because if // stop has been called from inside a queued callback, the _execute method // currenty running needs to call run() one more time for the 'complete' // event to be fired. // if stop is called from outside a callback, we need to explicitely call // run() once again to fire the 'complete' event. if (!this._executing) { this.run(); } return this; }, /** * Returns the current index of a callback. Pass in either the id or * callback function from getCallback. * * @method indexOf * @param callback {String|Function} the callback or its specified id * @return {Number} index of the callback or -1 if not found */ indexOf : function (callback) { var i = 0, len = this._q.length, c; for (; i < len; ++i) { c = this._q[i]; if (c === callback || c.id === callback) { return i; } } return -1; }, /** * Retrieve a callback by its id. Useful to modify the configuration * while the queue is running. * * @method getCallback * @param id {String} the id assigned to the callback * @return {Object} the callback object */ getCallback : function (id) { var i = this.indexOf(id); return (i > -1) ? this._q[i] : null; }, /** * Promotes the named callback to the top of the queue. If a callback is * currently executing or looping (via until or iterations), the promotion * is scheduled to occur after the current callback has completed. * * @method promote * @param callback {String|Object} the callback object or a callback's id * @return {AsyncQueue} the AsyncQueue instance * @chainable */ promote : function (callback) { var payload = { callback : callback },e; if (this.isRunning()) { e = this.after(SHIFT, function () { this.fire(PROMOTE, payload); e.detach(); }, this); } else { this.fire(PROMOTE, payload); } return this; }, /** * <p>Default functionality for the "promote" event. Promotes the * named callback to the head of the queue.</p> * * <p>The event object will contain a property "callback", which * holds the id of a callback or the callback object itself.</p> * * @method _defPromoteFn * @param e {Event} the custom event * @protected */ _defPromoteFn : function (e) { var i = this.indexOf(e.callback), promoted = (i > -1) ? this._q.splice(i,1)[0] : null; e.promoted = promoted; if (promoted) { this._q.unshift(promoted); } }, /** * Removes the callback from the queue. If the queue is active, the * removal is scheduled to occur after the current callback has completed. * * @method remove * @param callback {String|Object} the callback object or a callback's id * @return {AsyncQueue} the AsyncQueue instance * @chainable */ remove : function (callback) { var payload = { callback : callback },e; // Can't return the removed callback because of the deferral until // current callback is complete if (this.isRunning()) { e = this.after(SHIFT, function () { this.fire(REMOVE, payload); e.detach(); },this); } else { this.fire(REMOVE, payload); } return this; }, /** * <p>Default functionality for the "remove" event. Removes the * callback from the queue.</p> * * <p>The event object will contain a property "callback", which * holds the id of a callback or the callback object itself.</p> * * @method _defRemoveFn * @param e {Event} the custom event * @protected */ _defRemoveFn : function (e) { var i = this.indexOf(e.callback); e.removed = (i > -1) ? this._q.splice(i,1)[0] : null; }, /** * Returns the number of callbacks in the queue. * * @method size * @return {Number} */ size : function () { // next() flushes callbacks that have met their until() criteria and // therefore shouldn't count since they wouldn't execute anyway. if (!this.isRunning()) { this.next(); } return this._q.length; } });