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 */ 10 var AsyncUtil = {}; 11 12 /** 13 * Creates a class for executing several asynchronous closures in a fifo queue. 14 * Added tasks will be executed sequentially in order they were added. 15 * 16 * @constructor 17 */ 18 AsyncUtil.Queue = function() { 19 this.running_ = false; 20 this.closures_ = []; 21 }; 22 23 /** 24 * Enqueues a closure to be executed. 25 * @param {function(function())} closure Closure with a completion callback to 26 * be executed. 27 */ 28 AsyncUtil.Queue.prototype.run = function(closure) { 29 this.closures_.push(closure); 30 if (!this.running_) 31 this.continue_(); 32 }; 33 34 /** 35 * Serves the next closure from the queue. 36 * @private 37 */ 38 AsyncUtil.Queue.prototype.continue_ = function() { 39 if (!this.closures_.length) { 40 this.running_ = false; 41 return; 42 } 43 44 // Run the next closure. 45 this.running_ = true; 46 var closure = this.closures_.shift(); 47 closure(this.continue_.bind(this)); 48 }; 49 50 /** 51 * Creates a class for executing several asynchronous closures in a group in 52 * a dependency order. 53 * 54 * @constructor 55 */ 56 AsyncUtil.Group = function() { 57 this.addedTasks_ = {}; 58 this.pendingTasks_ = {}; 59 this.finishedTasks_ = {}; 60 this.completionCallbacks_ = []; 61 }; 62 63 /** 64 * Enqueues a closure to be executed after dependencies are completed. 65 * 66 * @param {function(function())} closure Closure with a completion callback to 67 * be executed. 68 * @param {Array.<string>=} opt_dependencies Array of dependencies. If no 69 * dependencies, then the the closure will be executed immediately. 70 * @param {string=} opt_name Task identifier. Specify to use in dependencies. 71 */ 72 AsyncUtil.Group.prototype.add = function(closure, opt_dependencies, opt_name) { 73 var length = Object.keys(this.addedTasks_).length; 74 var name = opt_name || ('(unnamed#' + (length + 1) + ')'); 75 76 var task = { 77 closure: closure, 78 dependencies: opt_dependencies || [], 79 name: name 80 }; 81 82 this.addedTasks_[name] = task; 83 this.pendingTasks_[name] = task; 84 }; 85 86 /** 87 * Runs the enqueued closured in order of dependencies. 88 * 89 * @param {function()=} opt_onCompletion Completion callback. 90 */ 91 AsyncUtil.Group.prototype.run = function(opt_onCompletion) { 92 if (opt_onCompletion) 93 this.completionCallbacks_.push(opt_onCompletion); 94 this.continue_(); 95 }; 96 97 /** 98 * Runs enqueued pending tasks whose dependencies are completed. 99 * @private 100 */ 101 AsyncUtil.Group.prototype.continue_ = function() { 102 // If all of the added tasks have finished, then call completion callbacks. 103 if (Object.keys(this.addedTasks_).length == 104 Object.keys(this.finishedTasks_).length) { 105 for (var index = 0; index < this.completionCallbacks_.length; index++) { 106 var callback = this.completionCallbacks_[index]; 107 callback(); 108 } 109 this.completionCallbacks_ = []; 110 return; 111 } 112 113 for (var name in this.pendingTasks_) { 114 var task = this.pendingTasks_[name]; 115 var dependencyMissing = false; 116 for (var index = 0; index < task.dependencies.length; index++) { 117 var dependency = task.dependencies[index]; 118 // Check if the dependency has finished. 119 if (!this.finishedTasks_[dependency]) 120 dependencyMissing = true; 121 } 122 // All dependences finished, therefore start the task. 123 if (!dependencyMissing) { 124 delete this.pendingTasks_[task.name]; 125 task.closure(this.finish_.bind(this, task)); 126 } 127 } 128 }; 129 130 /** 131 * Finishes the passed task and continues executing enqueued closures. 132 * 133 * @param {Object} task Task object. 134 * @private 135 */ 136 AsyncUtil.Group.prototype.finish_ = function(task) { 137 this.finishedTasks_[task.name] = task; 138 this.continue_(); 139 }; 140 141 /** 142 * Aggregates consecutive calls and executes the closure only once instead of 143 * several times. The first call is always called immediately, and the next 144 * consecutive ones are aggregated and the closure is called only once once 145 * |delay| amount of time passes after the last call to run(). 146 * 147 * @param {function()} closure Closure to be aggregated. 148 * @param {number=} opt_delay Minimum aggregation time in milliseconds. Default 149 * is 50 milliseconds. 150 * @constructor 151 */ 152 AsyncUtil.Aggregation = function(closure, opt_delay) { 153 /** 154 * @type {number} 155 * @private 156 */ 157 this.delay_ = opt_delay || 50; 158 159 /** 160 * @type {function()} 161 * @private 162 */ 163 this.closure_ = closure; 164 165 /** 166 * @type {number?} 167 * @private 168 */ 169 this.scheduledRunsTimer_ = null; 170 171 /** 172 * @type {number} 173 * @private 174 */ 175 this.lastRunTime_ = 0; 176 }; 177 178 /** 179 * Runs a closure. Skips consecutive calls. The first call is called 180 * immediately. 181 */ 182 AsyncUtil.Aggregation.prototype.run = function() { 183 // If recently called, then schedule the consecutive call with a delay. 184 if (Date.now() - this.lastRunTime_ < this.delay_) { 185 this.cancelScheduledRuns_(); 186 this.scheduledRunsTimer_ = setTimeout(this.runImmediately_.bind(this), 187 this.delay_ + 1); 188 this.lastRunTime_ = Date.now(); 189 return; 190 } 191 192 // Otherwise, run immediately. 193 this.runImmediately_(); 194 }; 195 196 /** 197 * Calls the schedule immediately and cancels any scheduled calls. 198 * @private 199 */ 200 AsyncUtil.Aggregation.prototype.runImmediately_ = function() { 201 this.cancelScheduledRuns_(); 202 this.closure_(); 203 this.lastRunTime_ = Date.now(); 204 }; 205 206 /** 207 * Cancels all scheduled runs (if any). 208 * @private 209 */ 210 AsyncUtil.Aggregation.prototype.cancelScheduledRuns_ = function() { 211 if (this.scheduledRunsTimer_) { 212 clearTimeout(this.scheduledRunsTimer_); 213 this.scheduledRunsTimer_ = null; 214 } 215 }; 216