blob: f74a9d493810a66f18f1bb0705665d0dfe80ad0e [file] [log] [blame]
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* Namespace for async utility functions.
*/
var AsyncUtil = {};
/**
* Asynchronous version of Array.forEach.
* This executes a provided function callback once per array element, then
* run completionCallback to notify the completion.
* The callback can be an asynchronous function, but the execution is
* sequentially done.
*
* @param {Array.<T>} array The array to be iterated.
* @param {function(function(), T, number, Array.<T>} callback The iteration
* callback. The first argument is a callback to notify the completion of
* the iteration.
* @param {function()} completionCallback Called when all iterations are
* completed.
* @param {Object=} opt_thisObject Bound to callback if given.
* @template T
*/
AsyncUtil.forEach = function(
array, callback, completionCallback, opt_thisObject) {
if (opt_thisObject)
callback = callback.bind(opt_thisObject);
var queue = new AsyncUtil.Queue();
for (var i = 0; i < array.length; i++) {
queue.run(function(element, index, iterationCompletionCallback) {
callback(iterationCompletionCallback, element, index, array);
}.bind(null, array[i], i));
}
queue.run(function(iterationCompletionCallback) {
completionCallback(); // Don't pass iteration completion callback.
});
};
/**
* Creates a class for executing several asynchronous closures in a fifo queue.
* Added tasks will be executed sequentially in order they were added.
*
* @constructor
*/
AsyncUtil.Queue = function() {
this.running_ = false;
this.closures_ = [];
};
/**
* @return {boolean} True when a task is running, otherwise false.
*/
AsyncUtil.Queue.prototype.isRunning = function() {
return this.running_;
};
/**
* Enqueues a closure to be executed.
* @param {function(function())} closure Closure with a completion callback to
* be executed.
*/
AsyncUtil.Queue.prototype.run = function(closure) {
this.closures_.push(closure);
if (!this.running_)
this.continue_();
};
/**
* Serves the next closure from the queue.
* @private
*/
AsyncUtil.Queue.prototype.continue_ = function() {
if (!this.closures_.length) {
this.running_ = false;
return;
}
// Run the next closure.
this.running_ = true;
var closure = this.closures_.shift();
closure(this.continue_.bind(this));
};
/**
* Cancels all pending tasks. Note that this does NOT cancel the task running
* currently.
*/
AsyncUtil.Queue.prototype.cancel = function() {
this.closures_ = [];
};
/**
* Creates a class for executing several asynchronous closures in a group in
* a dependency order.
*
* @constructor
*/
AsyncUtil.Group = function() {
this.addedTasks_ = {};
this.pendingTasks_ = {};
this.finishedTasks_ = {};
this.completionCallbacks_ = [];
};
/**
* Enqueues a closure to be executed after dependencies are completed.
*
* @param {function(function())} closure Closure with a completion callback to
* be executed.
* @param {Array.<string>=} opt_dependencies Array of dependencies. If no
* dependencies, then the the closure will be executed immediately.
* @param {string=} opt_name Task identifier. Specify to use in dependencies.
*/
AsyncUtil.Group.prototype.add = function(closure, opt_dependencies, opt_name) {
var length = Object.keys(this.addedTasks_).length;
var name = opt_name || ('(unnamed#' + (length + 1) + ')');
var task = {
closure: closure,
dependencies: opt_dependencies || [],
name: name
};
this.addedTasks_[name] = task;
this.pendingTasks_[name] = task;
};
/**
* Runs the enqueued closured in order of dependencies.
*
* @param {function()=} opt_onCompletion Completion callback.
*/
AsyncUtil.Group.prototype.run = function(opt_onCompletion) {
if (opt_onCompletion)
this.completionCallbacks_.push(opt_onCompletion);
this.continue_();
};
/**
* Runs enqueued pending tasks whose dependencies are completed.
* @private
*/
AsyncUtil.Group.prototype.continue_ = function() {
// If all of the added tasks have finished, then call completion callbacks.
if (Object.keys(this.addedTasks_).length ==
Object.keys(this.finishedTasks_).length) {
for (var index = 0; index < this.completionCallbacks_.length; index++) {
var callback = this.completionCallbacks_[index];
callback();
}
this.completionCallbacks_ = [];
return;
}
for (var name in this.pendingTasks_) {
var task = this.pendingTasks_[name];
var dependencyMissing = false;
for (var index = 0; index < task.dependencies.length; index++) {
var dependency = task.dependencies[index];
// Check if the dependency has finished.
if (!this.finishedTasks_[dependency])
dependencyMissing = true;
}
// All dependences finished, therefore start the task.
if (!dependencyMissing) {
delete this.pendingTasks_[task.name];
task.closure(this.finish_.bind(this, task));
}
}
};
/**
* Finishes the passed task and continues executing enqueued closures.
*
* @param {Object} task Task object.
* @private
*/
AsyncUtil.Group.prototype.finish_ = function(task) {
this.finishedTasks_[task.name] = task;
this.continue_();
};
/**
* Aggregates consecutive calls and executes the closure only once instead of
* several times. The first call is always called immediately, and the next
* consecutive ones are aggregated and the closure is called only once once
* |delay| amount of time passes after the last call to run().
*
* @param {function()} closure Closure to be aggregated.
* @param {number=} opt_delay Minimum aggregation time in milliseconds. Default
* is 50 milliseconds.
* @constructor
*/
AsyncUtil.Aggregation = function(closure, opt_delay) {
/**
* @type {number}
* @private
*/
this.delay_ = opt_delay || 50;
/**
* @type {function()}
* @private
*/
this.closure_ = closure;
/**
* @type {number?}
* @private
*/
this.scheduledRunsTimer_ = null;
/**
* @type {number}
* @private
*/
this.lastRunTime_ = 0;
};
/**
* Runs a closure. Skips consecutive calls. The first call is called
* immediately.
*/
AsyncUtil.Aggregation.prototype.run = function() {
// If recently called, then schedule the consecutive call with a delay.
if (Date.now() - this.lastRunTime_ < this.delay_) {
this.cancelScheduledRuns_();
this.scheduledRunsTimer_ = setTimeout(this.runImmediately_.bind(this),
this.delay_ + 1);
this.lastRunTime_ = Date.now();
return;
}
// Otherwise, run immediately.
this.runImmediately_();
};
/**
* Calls the schedule immediately and cancels any scheduled calls.
* @private
*/
AsyncUtil.Aggregation.prototype.runImmediately_ = function() {
this.cancelScheduledRuns_();
this.closure_();
this.lastRunTime_ = Date.now();
};
/**
* Cancels all scheduled runs (if any).
* @private
*/
AsyncUtil.Aggregation.prototype.cancelScheduledRuns_ = function() {
if (this.scheduledRunsTimer_) {
clearTimeout(this.scheduledRunsTimer_);
this.scheduledRunsTimer_ = null;
}
};