1// Copyright (c) 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5'use strict'; 6 7/** 8 * Namespace for async utility functions. 9 */ 10var AsyncUtil = {}; 11 12/** 13 * Asynchronous version of Array.forEach. 14 * This executes a provided function callback once per array element, then 15 * run completionCallback to notify the completion. 16 * The callback can be an asynchronous function, but the execution is 17 * sequentially done. 18 * 19 * @param {Array.<T>} array The array to be iterated. 20 * @param {function(function(), T, number, Array.<T>} callback The iteration 21 * callback. The first argument is a callback to notify the completion of 22 * the iteration. 23 * @param {function()} completionCallback Called when all iterations are 24 * completed. 25 * @param {Object=} opt_thisObject Bound to callback if given. 26 * @template T 27 */ 28AsyncUtil.forEach = function( 29 array, callback, completionCallback, opt_thisObject) { 30 if (opt_thisObject) 31 callback = callback.bind(opt_thisObject); 32 33 var queue = new AsyncUtil.Queue(); 34 for (var i = 0; i < array.length; i++) { 35 queue.run(function(element, index, iterationCompletionCallback) { 36 callback(iterationCompletionCallback, element, index, array); 37 }.bind(null, array[i], i)); 38 } 39 queue.run(function(iterationCompletionCallback) { 40 completionCallback(); // Don't pass iteration completion callback. 41 }); 42}; 43 44/** 45 * Creates a class for executing several asynchronous closures in a fifo queue. 46 * Added tasks will be executed sequentially in order they were added. 47 * 48 * @constructor 49 */ 50AsyncUtil.Queue = function() { 51 this.running_ = false; 52 this.closures_ = []; 53}; 54 55/** 56 * @return {boolean} True when a task is running, otherwise false. 57 */ 58AsyncUtil.Queue.prototype.isRunning = function() { 59 return this.running_; 60}; 61 62/** 63 * Enqueues a closure to be executed. 64 * @param {function(function())} closure Closure with a completion callback to 65 * be executed. 66 */ 67AsyncUtil.Queue.prototype.run = function(closure) { 68 this.closures_.push(closure); 69 if (!this.running_) 70 this.continue_(); 71}; 72 73/** 74 * Serves the next closure from the queue. 75 * @private 76 */ 77AsyncUtil.Queue.prototype.continue_ = function() { 78 if (!this.closures_.length) { 79 this.running_ = false; 80 return; 81 } 82 83 // Run the next closure. 84 this.running_ = true; 85 var closure = this.closures_.shift(); 86 closure(this.continue_.bind(this)); 87}; 88 89/** 90 * Cancels all pending tasks. Note that this does NOT cancel the task running 91 * currently. 92 */ 93AsyncUtil.Queue.prototype.cancel = function() { 94 this.closures_ = []; 95}; 96 97/** 98 * Creates a class for executing several asynchronous closures in a group in 99 * a dependency order. 100 * 101 * @constructor 102 */ 103AsyncUtil.Group = function() { 104 this.addedTasks_ = {}; 105 this.pendingTasks_ = {}; 106 this.finishedTasks_ = {}; 107 this.completionCallbacks_ = []; 108}; 109 110/** 111 * Enqueues a closure to be executed after dependencies are completed. 112 * 113 * @param {function(function())} closure Closure with a completion callback to 114 * be executed. 115 * @param {Array.<string>=} opt_dependencies Array of dependencies. If no 116 * dependencies, then the the closure will be executed immediately. 117 * @param {string=} opt_name Task identifier. Specify to use in dependencies. 118 */ 119AsyncUtil.Group.prototype.add = function(closure, opt_dependencies, opt_name) { 120 var length = Object.keys(this.addedTasks_).length; 121 var name = opt_name || ('(unnamed#' + (length + 1) + ')'); 122 123 var task = { 124 closure: closure, 125 dependencies: opt_dependencies || [], 126 name: name 127 }; 128 129 this.addedTasks_[name] = task; 130 this.pendingTasks_[name] = task; 131}; 132 133/** 134 * Runs the enqueued closured in order of dependencies. 135 * 136 * @param {function()=} opt_onCompletion Completion callback. 137 */ 138AsyncUtil.Group.prototype.run = function(opt_onCompletion) { 139 if (opt_onCompletion) 140 this.completionCallbacks_.push(opt_onCompletion); 141 this.continue_(); 142}; 143 144/** 145 * Runs enqueued pending tasks whose dependencies are completed. 146 * @private 147 */ 148AsyncUtil.Group.prototype.continue_ = function() { 149 // If all of the added tasks have finished, then call completion callbacks. 150 if (Object.keys(this.addedTasks_).length == 151 Object.keys(this.finishedTasks_).length) { 152 for (var index = 0; index < this.completionCallbacks_.length; index++) { 153 var callback = this.completionCallbacks_[index]; 154 callback(); 155 } 156 this.completionCallbacks_ = []; 157 return; 158 } 159 160 for (var name in this.pendingTasks_) { 161 var task = this.pendingTasks_[name]; 162 var dependencyMissing = false; 163 for (var index = 0; index < task.dependencies.length; index++) { 164 var dependency = task.dependencies[index]; 165 // Check if the dependency has finished. 166 if (!this.finishedTasks_[dependency]) 167 dependencyMissing = true; 168 } 169 // All dependences finished, therefore start the task. 170 if (!dependencyMissing) { 171 delete this.pendingTasks_[task.name]; 172 task.closure(this.finish_.bind(this, task)); 173 } 174 } 175}; 176 177/** 178 * Finishes the passed task and continues executing enqueued closures. 179 * 180 * @param {Object} task Task object. 181 * @private 182 */ 183AsyncUtil.Group.prototype.finish_ = function(task) { 184 this.finishedTasks_[task.name] = task; 185 this.continue_(); 186}; 187 188/** 189 * Aggregates consecutive calls and executes the closure only once instead of 190 * several times. The first call is always called immediately, and the next 191 * consecutive ones are aggregated and the closure is called only once once 192 * |delay| amount of time passes after the last call to run(). 193 * 194 * @param {function()} closure Closure to be aggregated. 195 * @param {number=} opt_delay Minimum aggregation time in milliseconds. Default 196 * is 50 milliseconds. 197 * @constructor 198 */ 199AsyncUtil.Aggregation = function(closure, opt_delay) { 200 /** 201 * @type {number} 202 * @private 203 */ 204 this.delay_ = opt_delay || 50; 205 206 /** 207 * @type {function()} 208 * @private 209 */ 210 this.closure_ = closure; 211 212 /** 213 * @type {number?} 214 * @private 215 */ 216 this.scheduledRunsTimer_ = null; 217 218 /** 219 * @type {number} 220 * @private 221 */ 222 this.lastRunTime_ = 0; 223}; 224 225/** 226 * Runs a closure. Skips consecutive calls. The first call is called 227 * immediately. 228 */ 229AsyncUtil.Aggregation.prototype.run = function() { 230 // If recently called, then schedule the consecutive call with a delay. 231 if (Date.now() - this.lastRunTime_ < this.delay_) { 232 this.cancelScheduledRuns_(); 233 this.scheduledRunsTimer_ = setTimeout(this.runImmediately_.bind(this), 234 this.delay_ + 1); 235 this.lastRunTime_ = Date.now(); 236 return; 237 } 238 239 // Otherwise, run immediately. 240 this.runImmediately_(); 241}; 242 243/** 244 * Calls the schedule immediately and cancels any scheduled calls. 245 * @private 246 */ 247AsyncUtil.Aggregation.prototype.runImmediately_ = function() { 248 this.cancelScheduledRuns_(); 249 this.closure_(); 250 this.lastRunTime_ = Date.now(); 251}; 252 253/** 254 * Cancels all scheduled runs (if any). 255 * @private 256 */ 257AsyncUtil.Aggregation.prototype.cancelScheduledRuns_ = function() { 258 if (this.scheduledRunsTimer_) { 259 clearTimeout(this.scheduledRunsTimer_); 260 this.scheduledRunsTimer_ = null; 261 } 262}; 263