OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 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 /** @suppress {duplicate} */ |
| 8 var base = base || {}; |
| 9 |
| 10 (function() { |
| 11 /** |
| 12 * A wrapper around a Promise object that keeps track of all |
| 13 * outstanding promises. This function is written to serve as a |
| 14 * drop-in replacement for the native Promise constructor. To create |
| 15 * a SpyPromise from an existing native Promise, use |
| 16 * SpyPromise.resolve. |
| 17 * |
| 18 * Note that this is a pseudo-constructor that actually returns a |
| 19 * regular promise with appropriate handlers attached. This detail |
| 20 * should be transparent when SpyPromise.activate has been called. |
| 21 * |
| 22 * The normal way to use this class is within a call to |
| 23 * SpyPromise.run, for example: |
| 24 * |
| 25 * base.SpyPromise.run(function() { |
| 26 * myCodeThatUsesPromises(); |
| 27 * }); |
| 28 * base.SpyPromise.settleAll().then(function() { |
| 29 * console.log('All promises have been settled!'); |
| 30 * }); |
| 31 * |
| 32 * @constructor |
| 33 * @extends {Promise} |
| 34 * @param {function(function(?):?, function(*):?):?} func A function |
| 35 * of the same type used as an argument to the native Promise |
| 36 * constructor, in other words, a function which is called |
| 37 * immediately, and whose arguments are a resolve function and a |
| 38 * reject function. |
| 39 */ |
| 40 base.SpyPromise = function(func) { |
| 41 var unsettled = new RealPromise(func); |
| 42 var unsettledId = remember(unsettled); |
| 43 return unsettled.then(function(/** * */value) { |
| 44 forget(unsettledId); |
| 45 return value; |
| 46 }, function(error) { |
| 47 forget(unsettledId); |
| 48 throw error; |
| 49 }); |
| 50 }; |
| 51 |
| 52 /** |
| 53 * The real promise constructor. Needed because it is normally hidden |
| 54 * by SpyPromise.activate or SpyPromise.run. |
| 55 * @const |
| 56 */ |
| 57 var RealPromise = Promise; |
| 58 |
| 59 /** |
| 60 * The real window.setTimeout method. Needed because some test |
| 61 * frameworks like to replace this method with a fake implementation. |
| 62 * @const |
| 63 */ |
| 64 var realSetTimeout = window.setTimeout.bind(window); |
| 65 |
| 66 /** |
| 67 * The number of unsettled promises. |
| 68 * @type {number} |
| 69 */ |
| 70 base.SpyPromise.unsettledCount; // initialized by reset() |
| 71 |
| 72 /** |
| 73 * A collection of all unsettled promises. |
| 74 * @type {!Object<number,!Promise>} |
| 75 */ |
| 76 var unsettled; // initialized by reset() |
| 77 |
| 78 /** |
| 79 * A counter used to assign ID numbers to new SpyPromise objects. |
| 80 * @type {number} |
| 81 */ |
| 82 var nextPromiseId; // initialized by reset() |
| 83 |
| 84 /** |
| 85 * A promise returned by SpyPromise.settleAll. |
| 86 * @type {Promise<null>} |
| 87 */ |
| 88 var settleAllPromise; // initialized by reset() |
| 89 |
| 90 /** |
| 91 * Records an unsettled promise. |
| 92 * |
| 93 * @param {!Promise} unsettledPromise |
| 94 * @return {number} The ID number to be passed to forget_. |
| 95 */ |
| 96 function remember(unsettledPromise) { |
| 97 var id = nextPromiseId++; |
| 98 if (unsettled[id] != null) { |
| 99 throw Error('Duplicate ID: ' + id); |
| 100 } |
| 101 base.SpyPromise.unsettledCount++; |
| 102 unsettled[id] = unsettledPromise; |
| 103 return id; |
| 104 }; |
| 105 |
| 106 /** |
| 107 * Forgets a promise. Called after the promise has been settled. |
| 108 * |
| 109 * @param {number} id |
| 110 * @private |
| 111 */ |
| 112 function forget(id) { |
| 113 console.assert(unsettled[id] != null, 'No such Promise: ' + id + '.'); |
| 114 base.SpyPromise.unsettledCount--; |
| 115 delete unsettled[id]; |
| 116 }; |
| 117 |
| 118 /** |
| 119 * Forgets about all unsettled promises. |
| 120 */ |
| 121 base.SpyPromise.reset = function() { |
| 122 base.SpyPromise.unsettledCount = 0; |
| 123 unsettled = {}; |
| 124 nextPromiseId = 0; |
| 125 settleAllPromise = null; |
| 126 }; |
| 127 |
| 128 // Initialize static variables. |
| 129 base.SpyPromise.reset(); |
| 130 |
| 131 /** |
| 132 * Tries to wait until all promises has been settled. |
| 133 * |
| 134 * @param {number=} opt_maxTimeMs The maximum number of milliseconds |
| 135 * (approximately) to wait (default: 1000). |
| 136 * @return {!Promise<null>} A real promise that is resolved when all |
| 137 * SpyPromises have been settled, or rejected after opt_maxTimeMs |
| 138 * milliseconds have elapsed. |
| 139 */ |
| 140 base.SpyPromise.settleAll = function(opt_maxTimeMs) { |
| 141 if (settleAllPromise) { |
| 142 return settleAllPromise; |
| 143 } |
| 144 |
| 145 var maxDelay = opt_maxTimeMs == null ? 1000 : opt_maxTimeMs; |
| 146 |
| 147 /** |
| 148 * @param {number} count |
| 149 * @param {number} totalDelay |
| 150 * @return {!Promise<null>} |
| 151 */ |
| 152 function loop(count, totalDelay) { |
| 153 return new RealPromise(function(resolve, reject) { |
| 154 if (base.SpyPromise.unsettledCount == 0) { |
| 155 settleAllPromise = null; |
| 156 resolve(null); |
| 157 } else if (totalDelay > maxDelay) { |
| 158 settleAllPromise = null; |
| 159 base.SpyPromise.reset(); |
| 160 reject(new Error('base.SpyPromise.settleAll timed out')); |
| 161 } else { |
| 162 // This implements quadratic backoff according to Euler's |
| 163 // triangular number formula. |
| 164 var delay = count; |
| 165 |
| 166 // Must jump through crazy hoops to get a real timer in a unit test. |
| 167 realSetTimeout(function() { |
| 168 resolve(loop( |
| 169 count + 1, |
| 170 delay + totalDelay)); |
| 171 }, delay); |
| 172 } |
| 173 }); |
| 174 }; |
| 175 |
| 176 // An extra promise needed here to prevent the loop function from |
| 177 // finishing before settleAllPromise is set. If that happens, |
| 178 // settleAllPromise will never be reset to null. |
| 179 settleAllPromise = RealPromise.resolve().then(function() { |
| 180 return loop(0, 0); |
| 181 }); |
| 182 return settleAllPromise; |
| 183 }; |
| 184 |
| 185 /** |
| 186 * Only for testing this class. Do not use. |
| 187 * @returns {boolean} True if settleAll is executing. |
| 188 */ |
| 189 base.SpyPromise.isSettleAllRunning = function() { |
| 190 return settleAllPromise != null; |
| 191 }; |
| 192 |
| 193 /** |
| 194 * Wrapper for Promise.resolve. |
| 195 * |
| 196 * @param {*} value |
| 197 * @return {!base.SpyPromise} |
| 198 */ |
| 199 base.SpyPromise.resolve = function(value) { |
| 200 return new base.SpyPromise(function(resolve, reject) { |
| 201 resolve(value); |
| 202 }); |
| 203 }; |
| 204 |
| 205 /** |
| 206 * Wrapper for Promise.reject. |
| 207 * |
| 208 * @param {*} value |
| 209 * @return {!base.SpyPromise} |
| 210 */ |
| 211 base.SpyPromise.reject = function(value) { |
| 212 return new base.SpyPromise(function(resolve, reject) { |
| 213 reject(value); |
| 214 }); |
| 215 }; |
| 216 |
| 217 /** |
| 218 * Wrapper for Promise.all. |
| 219 * |
| 220 * @param {!Array<Promise>} promises |
| 221 * @return {!base.SpyPromise} |
| 222 */ |
| 223 base.SpyPromise.all = function(promises) { |
| 224 return base.SpyPromise.resolve(RealPromise.all(promises)); |
| 225 }; |
| 226 |
| 227 /** |
| 228 * Wrapper for Promise.race. |
| 229 * |
| 230 * @param {!Array<Promise>} promises |
| 231 * @return {!base.SpyPromise} |
| 232 */ |
| 233 base.SpyPromise.race = function(promises) { |
| 234 return base.SpyPromise.resolve(RealPromise.race(promises)); |
| 235 }; |
| 236 |
| 237 /** |
| 238 * Sets Promise = base.SpyPromise. Must not be called more than once |
| 239 * without an intervening call to restore(). |
| 240 */ |
| 241 base.SpyPromise.activate = function() { |
| 242 if (settleAllPromise) { |
| 243 throw Error('called base.SpyPromise.activate while settleAll is running'); |
| 244 } |
| 245 if (Promise === base.SpyPromise) { |
| 246 throw Error('base.SpyPromise is already active'); |
| 247 } |
| 248 Promise = /** @type {function(new:Promise)} */(base.SpyPromise); |
| 249 }; |
| 250 |
| 251 /** |
| 252 * Restores the original value of Promise. |
| 253 */ |
| 254 base.SpyPromise.restore = function() { |
| 255 if (settleAllPromise) { |
| 256 throw Error('called base.SpyPromise.restore while settleAll is running'); |
| 257 } |
| 258 if (Promise === base.SpyPromise) { |
| 259 Promise = RealPromise; |
| 260 } else if (Promise === RealPromise) { |
| 261 throw new Error('base.SpyPromise is not active.'); |
| 262 } else { |
| 263 throw new Error('Something fishy is going on.'); |
| 264 } |
| 265 }; |
| 266 |
| 267 /** |
| 268 * Calls func with Promise equal to base.SpyPromise. |
| 269 * |
| 270 * @param {function():void} func A function which is expected to |
| 271 * create one or more promises. |
| 272 * @param {number=} opt_timeoutMs An optional timeout specifying how |
| 273 * long to wait for promise chains started in func to be settled. |
| 274 * (default: 1000 ms) |
| 275 * @return {!Promise<null>} A promise that is resolved after every |
| 276 * promise chain started in func is fully settled, or rejected |
| 277 * after a opt_timeoutMs. In any case, the original value of the |
| 278 * Promise constructor is restored before this promise is settled. |
| 279 */ |
| 280 base.SpyPromise.run = function(func, opt_timeoutMs) { |
| 281 base.SpyPromise.activate(); |
| 282 try { |
| 283 func(); |
| 284 } finally { |
| 285 return base.SpyPromise.settleAll(opt_timeoutMs).then(function() { |
| 286 base.SpyPromise.restore(); |
| 287 return null; |
| 288 }, function(error) { |
| 289 base.SpyPromise.restore(); |
| 290 throw error; |
| 291 }); |
| 292 } |
| 293 }; |
| 294 })(); |
OLD | NEW |