1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/modules/Promise-backend.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,901 @@ 1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +"use strict"; 1.11 + 1.12 +/** 1.13 + * This implementation file is imported by the Promise.jsm module, and as a 1.14 + * special case by the debugger server. To support chrome debugging, the 1.15 + * debugger server needs to have all its code in one global, so it must use 1.16 + * loadSubScript directly. 1.17 + * 1.18 + * In the general case, this script should be used by importing Promise.jsm: 1.19 + * 1.20 + * Components.utils.import("resource://gre/modules/Promise.jsm"); 1.21 + * 1.22 + * More documentation can be found in the Promise.jsm module. 1.23 + */ 1.24 + 1.25 +//////////////////////////////////////////////////////////////////////////////// 1.26 +//// Globals 1.27 + 1.28 +Cu.import("resource://gre/modules/Services.jsm"); 1.29 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.30 + 1.31 +const STATUS_PENDING = 0; 1.32 +const STATUS_RESOLVED = 1; 1.33 +const STATUS_REJECTED = 2; 1.34 + 1.35 +// These "private names" allow some properties of the Promise object to be 1.36 +// accessed only by this module, while still being visible on the object 1.37 +// manually when using a debugger. They don't strictly guarantee that the 1.38 +// properties are inaccessible by other code, but provide enough protection to 1.39 +// avoid using them by mistake. 1.40 +const salt = Math.floor(Math.random() * 100); 1.41 +const Name = (n) => "{private:" + n + ":" + salt + "}"; 1.42 +const N_STATUS = Name("status"); 1.43 +const N_VALUE = Name("value"); 1.44 +const N_HANDLERS = Name("handlers"); 1.45 +const N_WITNESS = Name("witness"); 1.46 + 1.47 + 1.48 +/////// Warn-upon-finalization mechanism 1.49 +// 1.50 +// One of the difficult problems with promises is locating uncaught 1.51 +// rejections. We adopt the following strategy: if a promise is rejected 1.52 +// at the time of its garbage-collection *and* if the promise is at the 1.53 +// end of a promise chain (i.e. |thatPromise.then| has never been 1.54 +// called), then we print a warning. 1.55 +// 1.56 +// let deferred = Promise.defer(); 1.57 +// let p = deferred.promise.then(); 1.58 +// deferred.reject(new Error("I am un uncaught error")); 1.59 +// deferred = null; 1.60 +// p = null; 1.61 +// 1.62 +// In this snippet, since |deferred.promise| is not the last in the 1.63 +// chain, no error will be reported for that promise. However, since 1.64 +// |p| is the last promise in the chain, the error will be reported 1.65 +// for |p|. 1.66 +// 1.67 +// Note that this may, in some cases, cause an error to be reported more 1.68 +// than once. For instance, consider: 1.69 +// 1.70 +// let deferred = Promise.defer(); 1.71 +// let p1 = deferred.promise.then(); 1.72 +// let p2 = deferred.promise.then(); 1.73 +// deferred.reject(new Error("I am an uncaught error")); 1.74 +// p1 = p2 = deferred = null; 1.75 +// 1.76 +// In this snippet, the error is reported both by p1 and by p2. 1.77 +// 1.78 + 1.79 +XPCOMUtils.defineLazyServiceGetter(this, "FinalizationWitnessService", 1.80 + "@mozilla.org/toolkit/finalizationwitness;1", 1.81 + "nsIFinalizationWitnessService"); 1.82 + 1.83 +let PendingErrors = { 1.84 + // An internal counter, used to generate unique id. 1.85 + _counter: 0, 1.86 + // Functions registered to be notified when a pending error 1.87 + // is reported as uncaught. 1.88 + _observers: new Set(), 1.89 + _map: new Map(), 1.90 + 1.91 + /** 1.92 + * Initialize PendingErrors 1.93 + */ 1.94 + init: function() { 1.95 + Services.obs.addObserver(function observe(aSubject, aTopic, aValue) { 1.96 + PendingErrors.report(aValue); 1.97 + }, "promise-finalization-witness", false); 1.98 + }, 1.99 + 1.100 + /** 1.101 + * Register an error as tracked. 1.102 + * 1.103 + * @return The unique identifier of the error. 1.104 + */ 1.105 + register: function(error) { 1.106 + let id = "pending-error-" + (this._counter++); 1.107 + // 1.108 + // At this stage, ideally, we would like to store the error itself 1.109 + // and delay any treatment until we are certain that we will need 1.110 + // to report that error. However, in the (unlikely but possible) 1.111 + // case the error holds a reference to the promise itself, doing so 1.112 + // would prevent the promise from being garbabe-collected, which 1.113 + // would both cause a memory leak and ensure that we cannot report 1.114 + // the uncaught error. 1.115 + // 1.116 + // To avoid this situation, we rather extract relevant data from 1.117 + // the error and further normalize it to strings. 1.118 + // 1.119 + let value = { 1.120 + date: new Date(), 1.121 + message: "" + error, 1.122 + fileName: null, 1.123 + stack: null, 1.124 + lineNumber: null 1.125 + }; 1.126 + try { // Defend against non-enumerable values 1.127 + if (error && error instanceof Ci.nsIException) { 1.128 + // nsIException does things a little differently. 1.129 + try { 1.130 + // For starters |.toString()| does not only contain the message, but 1.131 + // also the top stack frame, and we don't really want that. 1.132 + value.message = error.message; 1.133 + } catch (ex) { 1.134 + // Ignore field 1.135 + } 1.136 + try { 1.137 + // All lowercase filename. ;) 1.138 + value.fileName = error.filename; 1.139 + } catch (ex) { 1.140 + // Ignore field 1.141 + } 1.142 + try { 1.143 + value.lineNumber = error.lineNumber; 1.144 + } catch (ex) { 1.145 + // Ignore field 1.146 + } 1.147 + } else if (typeof error == "object" && error) { 1.148 + for (let k of ["fileName", "stack", "lineNumber"]) { 1.149 + try { // Defend against fallible getters and string conversions 1.150 + let v = error[k]; 1.151 + value[k] = v ? ("" + v) : null; 1.152 + } catch (ex) { 1.153 + // Ignore field 1.154 + } 1.155 + } 1.156 + } 1.157 + 1.158 + if (!value.stack) { 1.159 + // |error| is not an Error (or Error-alike). Try to figure out the stack. 1.160 + let stack = null; 1.161 + if (error && error.location && 1.162 + error.location instanceof Ci.nsIStackFrame) { 1.163 + // nsIException has full stack frames in the |.location| member. 1.164 + stack = error.location; 1.165 + } else { 1.166 + // Components.stack to the rescue! 1.167 + stack = Components.stack; 1.168 + // Remove those top frames that refer to Promise.jsm. 1.169 + while (stack) { 1.170 + if (!stack.filename.endsWith("/Promise.jsm")) { 1.171 + break; 1.172 + } 1.173 + stack = stack.caller; 1.174 + } 1.175 + } 1.176 + if (stack) { 1.177 + let frames = []; 1.178 + while (stack) { 1.179 + frames.push(stack); 1.180 + stack = stack.caller; 1.181 + } 1.182 + value.stack = frames.join("\n"); 1.183 + } 1.184 + } 1.185 + } catch (ex) { 1.186 + // Ignore value 1.187 + } 1.188 + this._map.set(id, value); 1.189 + return id; 1.190 + }, 1.191 + 1.192 + /** 1.193 + * Notify all observers that a pending error is now uncaught. 1.194 + * 1.195 + * @param id The identifier of the pending error, as returned by 1.196 + * |register|. 1.197 + */ 1.198 + report: function(id) { 1.199 + let value = this._map.get(id); 1.200 + if (!value) { 1.201 + return; // The error has already been reported 1.202 + } 1.203 + this._map.delete(id); 1.204 + for (let obs of this._observers.values()) { 1.205 + obs(value); 1.206 + } 1.207 + }, 1.208 + 1.209 + /** 1.210 + * Mark all pending errors are uncaught, notify the observers. 1.211 + */ 1.212 + flush: function() { 1.213 + // Since we are going to modify the map while walking it, 1.214 + // let's copying the keys first. 1.215 + let keys = [key for (key of this._map.keys())]; 1.216 + for (let key of keys) { 1.217 + this.report(key); 1.218 + } 1.219 + }, 1.220 + 1.221 + /** 1.222 + * Stop tracking an error, as this error has been caught, 1.223 + * eventually. 1.224 + */ 1.225 + unregister: function(id) { 1.226 + this._map.delete(id); 1.227 + }, 1.228 + 1.229 + /** 1.230 + * Add an observer notified when an error is reported as uncaught. 1.231 + * 1.232 + * @param {function} observer A function notified when an error is 1.233 + * reported as uncaught. Its arguments are 1.234 + * {message, date, fileName, stack, lineNumber} 1.235 + * All arguments are optional. 1.236 + */ 1.237 + addObserver: function(observer) { 1.238 + this._observers.add(observer); 1.239 + }, 1.240 + 1.241 + /** 1.242 + * Remove an observer added with addObserver 1.243 + */ 1.244 + removeObserver: function(observer) { 1.245 + this._observers.delete(observer); 1.246 + }, 1.247 + 1.248 + /** 1.249 + * Remove all the observers added with addObserver 1.250 + */ 1.251 + removeAllObservers: function() { 1.252 + this._observers.clear(); 1.253 + } 1.254 +}; 1.255 +PendingErrors.init(); 1.256 + 1.257 +// Default mechanism for displaying errors 1.258 +PendingErrors.addObserver(function(details) { 1.259 + let error = Cc['@mozilla.org/scripterror;1'].createInstance(Ci.nsIScriptError); 1.260 + if (!error || !Services.console) { 1.261 + // Too late during shutdown to use the nsIConsole 1.262 + dump("*************************\n"); 1.263 + dump("A promise chain failed to handle a rejection\n\n"); 1.264 + dump("On: " + details.date + "\n"); 1.265 + dump("Full message: " + details.message + "\n"); 1.266 + dump("See https://developer.mozilla.org/Mozilla/JavaScript_code_modules/Promise.jsm/Promise\n"); 1.267 + dump("Full stack: " + (details.stack||"not available") + "\n"); 1.268 + dump("*************************\n"); 1.269 + return; 1.270 + } 1.271 + let message = details.message; 1.272 + if (details.stack) { 1.273 + message += "\nFull Stack: " + details.stack; 1.274 + } 1.275 + error.init( 1.276 + /*message*/"A promise chain failed to handle a rejection.\n\n" + 1.277 + "Date: " + details.date + "\nFull Message: " + details.message, 1.278 + /*sourceName*/ details.fileName, 1.279 + /*sourceLine*/ details.lineNumber?("" + details.lineNumber):0, 1.280 + /*lineNumber*/ details.lineNumber || 0, 1.281 + /*columnNumber*/ 0, 1.282 + /*flags*/ Ci.nsIScriptError.errorFlag, 1.283 + /*category*/ "chrome javascript"); 1.284 + Services.console.logMessage(error); 1.285 +}); 1.286 + 1.287 + 1.288 +///////// Additional warnings for developers 1.289 +// 1.290 +// The following error types are considered programmer errors, which should be 1.291 +// reported (possibly redundantly) so as to let programmers fix their code. 1.292 +const ERRORS_TO_REPORT = ["EvalError", "RangeError", "ReferenceError", "TypeError"]; 1.293 + 1.294 +//////////////////////////////////////////////////////////////////////////////// 1.295 +//// Promise 1.296 + 1.297 +/** 1.298 + * The Promise constructor. Creates a new promise given an executor callback. 1.299 + * The executor callback is called with the resolve and reject handlers. 1.300 + * 1.301 + * @param aExecutor 1.302 + * The callback that will be called with resolve and reject. 1.303 + */ 1.304 +this.Promise = function Promise(aExecutor) 1.305 +{ 1.306 + if (typeof(aExecutor) != "function") { 1.307 + throw new TypeError("Promise constructor must be called with an executor."); 1.308 + } 1.309 + 1.310 + /* 1.311 + * Internal status of the promise. This can be equal to STATUS_PENDING, 1.312 + * STATUS_RESOLVED, or STATUS_REJECTED. 1.313 + */ 1.314 + Object.defineProperty(this, N_STATUS, { value: STATUS_PENDING, 1.315 + writable: true }); 1.316 + 1.317 + /* 1.318 + * When the N_STATUS property is STATUS_RESOLVED, this contains the final 1.319 + * resolution value, that cannot be a promise, because resolving with a 1.320 + * promise will cause its state to be eventually propagated instead. When the 1.321 + * N_STATUS property is STATUS_REJECTED, this contains the final rejection 1.322 + * reason, that could be a promise, even if this is uncommon. 1.323 + */ 1.324 + Object.defineProperty(this, N_VALUE, { writable: true }); 1.325 + 1.326 + /* 1.327 + * Array of Handler objects registered by the "then" method, and not processed 1.328 + * yet. Handlers are removed when the promise is resolved or rejected. 1.329 + */ 1.330 + Object.defineProperty(this, N_HANDLERS, { value: [] }); 1.331 + 1.332 + /** 1.333 + * When the N_STATUS property is STATUS_REJECTED and until there is 1.334 + * a rejection callback, this contains an array 1.335 + * - {string} id An id for use with |PendingErrors|; 1.336 + * - {FinalizationWitness} witness A witness broadcasting |id| on 1.337 + * notification "promise-finalization-witness". 1.338 + */ 1.339 + Object.defineProperty(this, N_WITNESS, { writable: true }); 1.340 + 1.341 + Object.seal(this); 1.342 + 1.343 + let resolve = PromiseWalker.completePromise 1.344 + .bind(PromiseWalker, this, STATUS_RESOLVED); 1.345 + let reject = PromiseWalker.completePromise 1.346 + .bind(PromiseWalker, this, STATUS_REJECTED); 1.347 + 1.348 + try { 1.349 + aExecutor.call(undefined, resolve, reject); 1.350 + } catch (ex) { 1.351 + reject(ex); 1.352 + } 1.353 +} 1.354 + 1.355 +/** 1.356 + * Calls one of the provided functions as soon as this promise is either 1.357 + * resolved or rejected. A new promise is returned, whose state evolves 1.358 + * depending on this promise and the provided callback functions. 1.359 + * 1.360 + * The appropriate callback is always invoked after this method returns, even 1.361 + * if this promise is already resolved or rejected. You can also call the 1.362 + * "then" method multiple times on the same promise, and the callbacks will be 1.363 + * invoked in the same order as they were registered. 1.364 + * 1.365 + * @param aOnResolve 1.366 + * If the promise is resolved, this function is invoked with the 1.367 + * resolution value of the promise as its only argument, and the 1.368 + * outcome of the function determines the state of the new promise 1.369 + * returned by the "then" method. In case this parameter is not a 1.370 + * function (usually "null"), the new promise returned by the "then" 1.371 + * method is resolved with the same value as the original promise. 1.372 + * 1.373 + * @param aOnReject 1.374 + * If the promise is rejected, this function is invoked with the 1.375 + * rejection reason of the promise as its only argument, and the 1.376 + * outcome of the function determines the state of the new promise 1.377 + * returned by the "then" method. In case this parameter is not a 1.378 + * function (usually left "undefined"), the new promise returned by the 1.379 + * "then" method is rejected with the same reason as the original 1.380 + * promise. 1.381 + * 1.382 + * @return A new promise that is initially pending, then assumes a state that 1.383 + * depends on the outcome of the invoked callback function: 1.384 + * - If the callback returns a value that is not a promise, including 1.385 + * "undefined", the new promise is resolved with this resolution 1.386 + * value, even if the original promise was rejected. 1.387 + * - If the callback throws an exception, the new promise is rejected 1.388 + * with the exception as the rejection reason, even if the original 1.389 + * promise was resolved. 1.390 + * - If the callback returns a promise, the new promise will 1.391 + * eventually assume the same state as the returned promise. 1.392 + * 1.393 + * @note If the aOnResolve callback throws an exception, the aOnReject 1.394 + * callback is not invoked. You can register a rejection callback on 1.395 + * the returned promise instead, to process any exception occurred in 1.396 + * either of the callbacks registered on this promise. 1.397 + */ 1.398 +Promise.prototype.then = function (aOnResolve, aOnReject) 1.399 +{ 1.400 + let handler = new Handler(this, aOnResolve, aOnReject); 1.401 + this[N_HANDLERS].push(handler); 1.402 + 1.403 + // Ensure the handler is scheduled for processing if this promise is already 1.404 + // resolved or rejected. 1.405 + if (this[N_STATUS] != STATUS_PENDING) { 1.406 + 1.407 + // This promise is not the last in the chain anymore. Remove any watchdog. 1.408 + if (this[N_WITNESS] != null) { 1.409 + let [id, witness] = this[N_WITNESS]; 1.410 + this[N_WITNESS] = null; 1.411 + witness.forget(); 1.412 + PendingErrors.unregister(id); 1.413 + } 1.414 + 1.415 + PromiseWalker.schedulePromise(this); 1.416 + } 1.417 + 1.418 + return handler.nextPromise; 1.419 +}; 1.420 + 1.421 +/** 1.422 + * Invokes `promise.then` with undefined for the resolve handler and a given 1.423 + * reject handler. 1.424 + * 1.425 + * @param aOnReject 1.426 + * The rejection handler. 1.427 + * 1.428 + * @return A new pending promise returned. 1.429 + * 1.430 + * @see Promise.prototype.then 1.431 + */ 1.432 +Promise.prototype.catch = function (aOnReject) 1.433 +{ 1.434 + return this.then(undefined, aOnReject); 1.435 +}; 1.436 + 1.437 +/** 1.438 + * Creates a new pending promise and provides methods to resolve or reject it. 1.439 + * 1.440 + * @return A new object, containing the new promise in the "promise" property, 1.441 + * and the methods to change its state in the "resolve" and "reject" 1.442 + * properties. See the Deferred documentation for details. 1.443 + */ 1.444 +Promise.defer = function () 1.445 +{ 1.446 + return new Deferred(); 1.447 +}; 1.448 + 1.449 +/** 1.450 + * Creates a new promise resolved with the specified value, or propagates the 1.451 + * state of an existing promise. 1.452 + * 1.453 + * @param aValue 1.454 + * If this value is not a promise, including "undefined", it becomes 1.455 + * the resolution value of the returned promise. If this value is a 1.456 + * promise, then the returned promise will eventually assume the same 1.457 + * state as the provided promise. 1.458 + * 1.459 + * @return A promise that can be pending, resolved, or rejected. 1.460 + */ 1.461 +Promise.resolve = function (aValue) 1.462 +{ 1.463 + if (aValue && typeof(aValue) == "function" && aValue.isAsyncFunction) { 1.464 + throw new TypeError( 1.465 + "Cannot resolve a promise with an async function. " + 1.466 + "You should either invoke the async function first " + 1.467 + "or use 'Task.spawn' instead of 'Task.async' to start " + 1.468 + "the Task and return its promise."); 1.469 + } 1.470 + 1.471 + if (aValue instanceof Promise) { 1.472 + return aValue; 1.473 + } 1.474 + 1.475 + return new Promise((aResolve) => aResolve(aValue)); 1.476 +}; 1.477 + 1.478 +/** 1.479 + * Creates a new promise rejected with the specified reason. 1.480 + * 1.481 + * @param aReason 1.482 + * The rejection reason for the returned promise. Although the reason 1.483 + * can be "undefined", it is generally an Error object, like in 1.484 + * exception handling. 1.485 + * 1.486 + * @return A rejected promise. 1.487 + * 1.488 + * @note The aReason argument should not be a promise. Using a rejected 1.489 + * promise for the value of aReason would make the rejection reason 1.490 + * equal to the rejected promise itself, and not its rejection reason. 1.491 + */ 1.492 +Promise.reject = function (aReason) 1.493 +{ 1.494 + return new Promise((_, aReject) => aReject(aReason)); 1.495 +}; 1.496 + 1.497 +/** 1.498 + * Returns a promise that is resolved or rejected when all values are 1.499 + * resolved or any is rejected. 1.500 + * 1.501 + * @param aValues 1.502 + * Iterable of promises that may be pending, resolved, or rejected. When 1.503 + * all are resolved or any is rejected, the returned promise will be 1.504 + * resolved or rejected as well. 1.505 + * 1.506 + * @return A new promise that is fulfilled when all values are resolved or 1.507 + * that is rejected when any of the values are rejected. Its 1.508 + * resolution value will be an array of all resolved values in the 1.509 + * given order, or undefined if aValues is an empty array. The reject 1.510 + * reason will be forwarded from the first promise in the list of 1.511 + * given promises to be rejected. 1.512 + */ 1.513 +Promise.all = function (aValues) 1.514 +{ 1.515 + if (aValues == null || typeof(aValues["@@iterator"]) != "function") { 1.516 + throw new Error("Promise.all() expects an iterable."); 1.517 + } 1.518 + 1.519 + return new Promise((resolve, reject) => { 1.520 + let values = Array.isArray(aValues) ? aValues : [...aValues]; 1.521 + let countdown = values.length; 1.522 + let resolutionValues = new Array(countdown); 1.523 + 1.524 + if (!countdown) { 1.525 + resolve(resolutionValues); 1.526 + return; 1.527 + } 1.528 + 1.529 + function checkForCompletion(aValue, aIndex) { 1.530 + resolutionValues[aIndex] = aValue; 1.531 + if (--countdown === 0) { 1.532 + resolve(resolutionValues); 1.533 + } 1.534 + } 1.535 + 1.536 + for (let i = 0; i < values.length; i++) { 1.537 + let index = i; 1.538 + let value = values[i]; 1.539 + let resolver = val => checkForCompletion(val, index); 1.540 + 1.541 + if (value && typeof(value.then) == "function") { 1.542 + value.then(resolver, reject); 1.543 + } else { 1.544 + // Given value is not a promise, forward it as a resolution value. 1.545 + resolver(value); 1.546 + } 1.547 + } 1.548 + }); 1.549 +}; 1.550 + 1.551 +/** 1.552 + * Returns a promise that is resolved or rejected when the first value is 1.553 + * resolved or rejected, taking on the value or reason of that promise. 1.554 + * 1.555 + * @param aValues 1.556 + * Iterable of values or promises that may be pending, resolved, or 1.557 + * rejected. When any is resolved or rejected, the returned promise will 1.558 + * be resolved or rejected as to the given value or reason. 1.559 + * 1.560 + * @return A new promise that is fulfilled when any values are resolved or 1.561 + * rejected. Its resolution value will be forwarded from the resolution 1.562 + * value or rejection reason. 1.563 + */ 1.564 +Promise.race = function (aValues) 1.565 +{ 1.566 + if (aValues == null || typeof(aValues["@@iterator"]) != "function") { 1.567 + throw new Error("Promise.race() expects an iterable."); 1.568 + } 1.569 + 1.570 + return new Promise((resolve, reject) => { 1.571 + for (let value of aValues) { 1.572 + Promise.resolve(value).then(resolve, reject); 1.573 + } 1.574 + }); 1.575 +}; 1.576 + 1.577 +Promise.Debugging = { 1.578 + /** 1.579 + * Add an observer notified when an error is reported as uncaught. 1.580 + * 1.581 + * @param {function} observer A function notified when an error is 1.582 + * reported as uncaught. Its arguments are 1.583 + * {message, date, fileName, stack, lineNumber} 1.584 + * All arguments are optional. 1.585 + */ 1.586 + addUncaughtErrorObserver: function(observer) { 1.587 + PendingErrors.addObserver(observer); 1.588 + }, 1.589 + 1.590 + /** 1.591 + * Remove an observer added with addUncaughtErrorObserver 1.592 + * 1.593 + * @param {function} An observer registered with 1.594 + * addUncaughtErrorObserver. 1.595 + */ 1.596 + removeUncaughtErrorObserver: function(observer) { 1.597 + PendingErrors.removeObserver(observer); 1.598 + }, 1.599 + 1.600 + /** 1.601 + * Remove all the observers added with addUncaughtErrorObserver 1.602 + */ 1.603 + clearUncaughtErrorObservers: function() { 1.604 + PendingErrors.removeAllObservers(); 1.605 + }, 1.606 + 1.607 + /** 1.608 + * Force all pending errors to be reported immediately as uncaught. 1.609 + * Note that this may cause some false positives. 1.610 + */ 1.611 + flushUncaughtErrors: function() { 1.612 + PendingErrors.flush(); 1.613 + }, 1.614 +}; 1.615 +Object.freeze(Promise.Debugging); 1.616 + 1.617 +Object.freeze(Promise); 1.618 + 1.619 +//////////////////////////////////////////////////////////////////////////////// 1.620 +//// PromiseWalker 1.621 + 1.622 +/** 1.623 + * This singleton object invokes the handlers registered on resolved and 1.624 + * rejected promises, ensuring that processing is not recursive and is done in 1.625 + * the same order as registration occurred on each promise. 1.626 + * 1.627 + * There is no guarantee on the order of execution of handlers registered on 1.628 + * different promises. 1.629 + */ 1.630 +this.PromiseWalker = { 1.631 + /** 1.632 + * Singleton array of all the unprocessed handlers currently registered on 1.633 + * resolved or rejected promises. Handlers are removed from the array as soon 1.634 + * as they are processed. 1.635 + */ 1.636 + handlers: [], 1.637 + 1.638 + /** 1.639 + * Called when a promise needs to change state to be resolved or rejected. 1.640 + * 1.641 + * @param aPromise 1.642 + * Promise that needs to change state. If this is already resolved or 1.643 + * rejected, this method has no effect. 1.644 + * @param aStatus 1.645 + * New desired status, either STATUS_RESOLVED or STATUS_REJECTED. 1.646 + * @param aValue 1.647 + * Associated resolution value or rejection reason. 1.648 + */ 1.649 + completePromise: function (aPromise, aStatus, aValue) 1.650 + { 1.651 + // Do nothing if the promise is already resolved or rejected. 1.652 + if (aPromise[N_STATUS] != STATUS_PENDING) { 1.653 + return; 1.654 + } 1.655 + 1.656 + // Resolving with another promise will cause this promise to eventually 1.657 + // assume the state of the provided promise. 1.658 + if (aStatus == STATUS_RESOLVED && aValue && 1.659 + typeof(aValue.then) == "function") { 1.660 + aValue.then(this.completePromise.bind(this, aPromise, STATUS_RESOLVED), 1.661 + this.completePromise.bind(this, aPromise, STATUS_REJECTED)); 1.662 + return; 1.663 + } 1.664 + 1.665 + // Change the promise status and schedule our handlers for processing. 1.666 + aPromise[N_STATUS] = aStatus; 1.667 + aPromise[N_VALUE] = aValue; 1.668 + if (aPromise[N_HANDLERS].length > 0) { 1.669 + this.schedulePromise(aPromise); 1.670 + } else if (aStatus == STATUS_REJECTED) { 1.671 + // This is a rejection and the promise is the last in the chain. 1.672 + // For the time being we therefore have an uncaught error. 1.673 + let id = PendingErrors.register(aValue); 1.674 + let witness = 1.675 + FinalizationWitnessService.make("promise-finalization-witness", id); 1.676 + aPromise[N_WITNESS] = [id, witness]; 1.677 + } 1.678 + }, 1.679 + 1.680 + /** 1.681 + * Sets up the PromiseWalker loop to start on the next tick of the event loop 1.682 + */ 1.683 + scheduleWalkerLoop: function() 1.684 + { 1.685 + this.walkerLoopScheduled = true; 1.686 + Services.tm.currentThread.dispatch(this.walkerLoop, 1.687 + Ci.nsIThread.DISPATCH_NORMAL); 1.688 + }, 1.689 + 1.690 + /** 1.691 + * Schedules the resolution or rejection handlers registered on the provided 1.692 + * promise for processing. 1.693 + * 1.694 + * @param aPromise 1.695 + * Resolved or rejected promise whose handlers should be processed. It 1.696 + * is expected that this promise has at least one handler to process. 1.697 + */ 1.698 + schedulePromise: function (aPromise) 1.699 + { 1.700 + // Migrate the handlers from the provided promise to the global list. 1.701 + for (let handler of aPromise[N_HANDLERS]) { 1.702 + this.handlers.push(handler); 1.703 + } 1.704 + aPromise[N_HANDLERS].length = 0; 1.705 + 1.706 + // Schedule the walker loop on the next tick of the event loop. 1.707 + if (!this.walkerLoopScheduled) { 1.708 + this.scheduleWalkerLoop(); 1.709 + } 1.710 + }, 1.711 + 1.712 + /** 1.713 + * Indicates whether the walker loop is currently scheduled for execution on 1.714 + * the next tick of the event loop. 1.715 + */ 1.716 + walkerLoopScheduled: false, 1.717 + 1.718 + /** 1.719 + * Processes all the known handlers during this tick of the event loop. This 1.720 + * eager processing is done to avoid unnecessarily exiting and re-entering the 1.721 + * JavaScript context for each handler on a resolved or rejected promise. 1.722 + * 1.723 + * This function is called with "this" bound to the PromiseWalker object. 1.724 + */ 1.725 + walkerLoop: function () 1.726 + { 1.727 + // If there is more than one handler waiting, reschedule the walker loop 1.728 + // immediately. Otherwise, use walkerLoopScheduled to tell schedulePromise() 1.729 + // to reschedule the loop if it adds more handlers to the queue. This makes 1.730 + // this walker resilient to the case where one handler does not return, but 1.731 + // starts a nested event loop. In that case, the newly scheduled walker will 1.732 + // take over. In the common case, the newly scheduled walker will be invoked 1.733 + // after this one has returned, with no actual handler to process. This 1.734 + // small overhead is required to make nested event loops work correctly, but 1.735 + // occurs at most once per resolution chain, thus having only a minor 1.736 + // impact on overall performance. 1.737 + if (this.handlers.length > 1) { 1.738 + this.scheduleWalkerLoop(); 1.739 + } else { 1.740 + this.walkerLoopScheduled = false; 1.741 + } 1.742 + 1.743 + // Process all the known handlers eagerly. 1.744 + while (this.handlers.length > 0) { 1.745 + this.handlers.shift().process(); 1.746 + } 1.747 + }, 1.748 +}; 1.749 + 1.750 +// Bind the function to the singleton once. 1.751 +PromiseWalker.walkerLoop = PromiseWalker.walkerLoop.bind(PromiseWalker); 1.752 + 1.753 +//////////////////////////////////////////////////////////////////////////////// 1.754 +//// Deferred 1.755 + 1.756 +/** 1.757 + * Returned by "Promise.defer" to provide a new promise along with methods to 1.758 + * change its state. 1.759 + */ 1.760 +function Deferred() 1.761 +{ 1.762 + this.promise = new Promise((aResolve, aReject) => { 1.763 + this.resolve = aResolve; 1.764 + this.reject = aReject; 1.765 + }); 1.766 + Object.freeze(this); 1.767 +} 1.768 + 1.769 +Deferred.prototype = { 1.770 + /** 1.771 + * A newly created promise, initially in the pending state. 1.772 + */ 1.773 + promise: null, 1.774 + 1.775 + /** 1.776 + * Resolves the associated promise with the specified value, or propagates the 1.777 + * state of an existing promise. If the associated promise has already been 1.778 + * resolved or rejected, this method does nothing. 1.779 + * 1.780 + * This function is bound to its associated promise when "Promise.defer" is 1.781 + * called, and can be called with any value of "this". 1.782 + * 1.783 + * @param aValue 1.784 + * If this value is not a promise, including "undefined", it becomes 1.785 + * the resolution value of the associated promise. If this value is a 1.786 + * promise, then the associated promise will eventually assume the same 1.787 + * state as the provided promise. 1.788 + * 1.789 + * @note Calling this method with a pending promise as the aValue argument, 1.790 + * and then calling it again with another value before the promise is 1.791 + * resolved or rejected, has unspecified behavior and should be avoided. 1.792 + */ 1.793 + resolve: null, 1.794 + 1.795 + /** 1.796 + * Rejects the associated promise with the specified reason. If the promise 1.797 + * has already been resolved or rejected, this method does nothing. 1.798 + * 1.799 + * This function is bound to its associated promise when "Promise.defer" is 1.800 + * called, and can be called with any value of "this". 1.801 + * 1.802 + * @param aReason 1.803 + * The rejection reason for the associated promise. Although the 1.804 + * reason can be "undefined", it is generally an Error object, like in 1.805 + * exception handling. 1.806 + * 1.807 + * @note The aReason argument should not generally be a promise. In fact, 1.808 + * using a rejected promise for the value of aReason would make the 1.809 + * rejection reason equal to the rejected promise itself, not to the 1.810 + * rejection reason of the rejected promise. 1.811 + */ 1.812 + reject: null, 1.813 +}; 1.814 + 1.815 +//////////////////////////////////////////////////////////////////////////////// 1.816 +//// Handler 1.817 + 1.818 +/** 1.819 + * Handler registered on a promise by the "then" function. 1.820 + */ 1.821 +function Handler(aThisPromise, aOnResolve, aOnReject) 1.822 +{ 1.823 + this.thisPromise = aThisPromise; 1.824 + this.onResolve = aOnResolve; 1.825 + this.onReject = aOnReject; 1.826 + this.nextPromise = new Promise(() => {}); 1.827 +} 1.828 + 1.829 +Handler.prototype = { 1.830 + /** 1.831 + * Promise on which the "then" method was called. 1.832 + */ 1.833 + thisPromise: null, 1.834 + 1.835 + /** 1.836 + * Unmodified resolution handler provided to the "then" method. 1.837 + */ 1.838 + onResolve: null, 1.839 + 1.840 + /** 1.841 + * Unmodified rejection handler provided to the "then" method. 1.842 + */ 1.843 + onReject: null, 1.844 + 1.845 + /** 1.846 + * New promise that will be returned by the "then" method. 1.847 + */ 1.848 + nextPromise: null, 1.849 + 1.850 + /** 1.851 + * Called after thisPromise is resolved or rejected, invokes the appropriate 1.852 + * callback and propagates the result to nextPromise. 1.853 + */ 1.854 + process: function() 1.855 + { 1.856 + // The state of this promise is propagated unless a handler is defined. 1.857 + let nextStatus = this.thisPromise[N_STATUS]; 1.858 + let nextValue = this.thisPromise[N_VALUE]; 1.859 + 1.860 + try { 1.861 + // If a handler is defined for either resolution or rejection, invoke it 1.862 + // to determine the state of the next promise, that will be resolved with 1.863 + // the returned value, that can also be another promise. 1.864 + if (nextStatus == STATUS_RESOLVED) { 1.865 + if (typeof(this.onResolve) == "function") { 1.866 + nextValue = this.onResolve.call(undefined, nextValue); 1.867 + } 1.868 + } else if (typeof(this.onReject) == "function") { 1.869 + nextValue = this.onReject.call(undefined, nextValue); 1.870 + nextStatus = STATUS_RESOLVED; 1.871 + } 1.872 + } catch (ex) { 1.873 + 1.874 + // An exception has occurred in the handler. 1.875 + 1.876 + if (ex && typeof ex == "object" && "name" in ex && 1.877 + ERRORS_TO_REPORT.indexOf(ex.name) != -1) { 1.878 + 1.879 + // We suspect that the exception is a programmer error, so we now 1.880 + // display it using dump(). Note that we do not use Cu.reportError as 1.881 + // we assume that this is a programming error, so we do not want end 1.882 + // users to see it. Also, if the programmer handles errors correctly, 1.883 + // they will either treat the error or log them somewhere. 1.884 + 1.885 + dump("*************************\n"); 1.886 + dump("A coding exception was thrown in a Promise " + 1.887 + ((nextStatus == STATUS_RESOLVED) ? "resolution":"rejection") + 1.888 + " callback.\n\n"); 1.889 + dump("Full message: " + ex + "\n"); 1.890 + dump("See https://developer.mozilla.org/Mozilla/JavaScript_code_modules/Promise.jsm/Promise\n"); 1.891 + dump("Full stack: " + (("stack" in ex)?ex.stack:"not available") + "\n"); 1.892 + dump("*************************\n"); 1.893 + 1.894 + } 1.895 + 1.896 + // Additionally, reject the next promise. 1.897 + nextStatus = STATUS_REJECTED; 1.898 + nextValue = ex; 1.899 + } 1.900 + 1.901 + // Propagate the newly determined state to the next promise. 1.902 + PromiseWalker.completePromise(this.nextPromise, nextStatus, nextValue); 1.903 + }, 1.904 +};