1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
10 * This implementation file is imported by the Promise.jsm module, and as a
11 * special case by the debugger server. To support chrome debugging, the
12 * debugger server needs to have all its code in one global, so it must use
13 * loadSubScript directly.
15 * In the general case, this script should be used by importing Promise.jsm:
17 * Components.utils.import("resource://gre/modules/Promise.jsm");
19 * More documentation can be found in the Promise.jsm module.
22 ////////////////////////////////////////////////////////////////////////////////
25 Cu.import("resource://gre/modules/Services.jsm");
26 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
28 const STATUS_PENDING = 0;
29 const STATUS_RESOLVED = 1;
30 const STATUS_REJECTED = 2;
32 // This N_INTERNALS name allow internal properties of the Promise to be
33 // accessed only by this module, while still being visible on the object
34 // manually when using a debugger. This doesn't strictly guarantee that the
35 // properties are inaccessible by other code, but provide enough protection to
36 // avoid using them by mistake.
37 const salt = Math.floor(Math.random() * 100);
38 const N_INTERNALS = "{private:internals:" + salt + "}";
40 /////// Warn-upon-finalization mechanism
42 // One of the difficult problems with promises is locating uncaught
43 // rejections. We adopt the following strategy: if a promise is rejected
44 // at the time of its garbage-collection *and* if the promise is at the
45 // end of a promise chain (i.e. |thatPromise.then| has never been
46 // called), then we print a warning.
48 // let deferred = Promise.defer();
49 // let p = deferred.promise.then();
50 // deferred.reject(new Error("I am un uncaught error"));
54 // In this snippet, since |deferred.promise| is not the last in the
55 // chain, no error will be reported for that promise. However, since
56 // |p| is the last promise in the chain, the error will be reported
59 // Note that this may, in some cases, cause an error to be reported more
60 // than once. For instance, consider:
62 // let deferred = Promise.defer();
63 // let p1 = deferred.promise.then();
64 // let p2 = deferred.promise.then();
65 // deferred.reject(new Error("I am an uncaught error"));
66 // p1 = p2 = deferred = null;
68 // In this snippet, the error is reported both by p1 and by p2.
71 XPCOMUtils.defineLazyServiceGetter(this, "FinalizationWitnessService",
72 "@mozilla.org/toolkit/finalizationwitness;1",
73 "nsIFinalizationWitnessService");
76 // An internal counter, used to generate unique id.
78 // Functions registered to be notified when a pending error
79 // is reported as uncaught.
80 _observers: new Set(),
84 * Initialize PendingErrors
87 Services.obs.addObserver(function observe(aSubject, aTopic, aValue) {
88 PendingErrors.report(aValue);
89 }, "promise-finalization-witness", false);
93 * Register an error as tracked.
95 * @return The unique identifier of the error.
97 register: function(error) {
98 let id = "pending-error-" + (this._counter++);
100 // At this stage, ideally, we would like to store the error itself
101 // and delay any treatment until we are certain that we will need
102 // to report that error. However, in the (unlikely but possible)
103 // case the error holds a reference to the promise itself, doing so
104 // would prevent the promise from being garbabe-collected, which
105 // would both cause a memory leak and ensure that we cannot report
106 // the uncaught error.
108 // To avoid this situation, we rather extract relevant data from
109 // the error and further normalize it to strings.
118 try { // Defend against non-enumerable values
119 if (error && error instanceof Ci.nsIException) {
120 // nsIException does things a little differently.
122 // For starters |.toString()| does not only contain the message, but
123 // also the top stack frame, and we don't really want that.
124 value.message = error.message;
129 // All lowercase filename. ;)
130 value.fileName = error.filename;
135 value.lineNumber = error.lineNumber;
139 } else if (typeof error == "object" && error) {
140 for (let k of ["fileName", "stack", "lineNumber"]) {
141 try { // Defend against fallible getters and string conversions
143 value[k] = v ? ("" + v) : null;
151 // |error| is not an Error (or Error-alike). Try to figure out the stack.
153 if (error && error.location &&
154 error.location instanceof Ci.nsIStackFrame) {
155 // nsIException has full stack frames in the |.location| member.
156 stack = error.location;
158 // Components.stack to the rescue!
159 stack = Components.stack;
160 // Remove those top frames that refer to Promise.jsm.
162 if (!stack.filename.endsWith("/Promise.jsm")) {
165 stack = stack.caller;
172 stack = stack.caller;
174 value.stack = frames.join("\n");
180 this._map.set(id, value);
185 * Notify all observers that a pending error is now uncaught.
187 * @param id The identifier of the pending error, as returned by
190 report: function(id) {
191 let value = this._map.get(id);
193 return; // The error has already been reported
195 this._map.delete(id);
196 for (let obs of this._observers.values()) {
202 * Mark all pending errors are uncaught, notify the observers.
205 // Since we are going to modify the map while walking it,
206 // let's copying the keys first.
207 let keys = [key for (key of this._map.keys())];
208 for (let key of keys) {
214 * Stop tracking an error, as this error has been caught,
217 unregister: function(id) {
218 this._map.delete(id);
222 * Add an observer notified when an error is reported as uncaught.
224 * @param {function} observer A function notified when an error is
225 * reported as uncaught. Its arguments are
226 * {message, date, fileName, stack, lineNumber}
227 * All arguments are optional.
229 addObserver: function(observer) {
230 this._observers.add(observer);
234 * Remove an observer added with addObserver
236 removeObserver: function(observer) {
237 this._observers.delete(observer);
241 * Remove all the observers added with addObserver
243 removeAllObservers: function() {
244 this._observers.clear();
247 PendingErrors.init();
249 // Default mechanism for displaying errors
250 PendingErrors.addObserver(function(details) {
251 const generalDescription = "A promise chain failed to handle a rejection." +
252 " Did you forget to '.catch', or did you forget to 'return'?\nSee" +
253 " https://developer.mozilla.org/Mozilla/JavaScript_code_modules/Promise.jsm/Promise\n\n";
255 let error = Cc['@mozilla.org/scripterror;1'].createInstance(Ci.nsIScriptError);
256 if (!error || !Services.console) {
257 // Too late during shutdown to use the nsIConsole
258 dump("*************************\n");
259 dump(generalDescription);
260 dump("On: " + details.date + "\n");
261 dump("Full message: " + details.message + "\n");
262 dump("Full stack: " + (details.stack||"not available") + "\n");
263 dump("*************************\n");
266 let message = details.message;
268 message += "\nFull Stack: " + details.stack;
271 /*message*/ generalDescription +
272 "Date: " + details.date + "\nFull Message: " + details.message,
273 /*sourceName*/ details.fileName,
274 /*sourceLine*/ details.lineNumber?("" + details.lineNumber):0,
275 /*lineNumber*/ details.lineNumber || 0,
277 /*flags*/ Ci.nsIScriptError.errorFlag,
278 /*category*/ "chrome javascript");
279 Services.console.logMessage(error);
283 ///////// Additional warnings for developers
285 // The following error types are considered programmer errors, which should be
286 // reported (possibly redundantly) so as to let programmers fix their code.
287 const ERRORS_TO_REPORT = ["EvalError", "RangeError", "ReferenceError", "TypeError"];
289 ////////////////////////////////////////////////////////////////////////////////
293 * The Promise constructor. Creates a new promise given an executor callback.
294 * The executor callback is called with the resolve and reject handlers.
297 * The callback that will be called with resolve and reject.
299 this.Promise = function Promise(aExecutor)
301 if (typeof(aExecutor) != "function") {
302 throw new TypeError("Promise constructor must be called with an executor.");
306 * Object holding all of our internal values we associate with the promise.
308 Object.defineProperty(this, N_INTERNALS, { value: {
310 * Internal status of the promise. This can be equal to STATUS_PENDING,
311 * STATUS_RESOLVED, or STATUS_REJECTED.
313 status: STATUS_PENDING,
316 * When the status property is STATUS_RESOLVED, this contains the final
317 * resolution value, that cannot be a promise, because resolving with a
318 * promise will cause its state to be eventually propagated instead. When the
319 * status property is STATUS_REJECTED, this contains the final rejection
320 * reason, that could be a promise, even if this is uncommon.
325 * Array of Handler objects registered by the "then" method, and not processed
326 * yet. Handlers are removed when the promise is resolved or rejected.
331 * When the status property is STATUS_REJECTED and until there is
332 * a rejection callback, this contains an array
333 * - {string} id An id for use with |PendingErrors|;
334 * - {FinalizationWitness} witness A witness broadcasting |id| on
335 * notification "promise-finalization-witness".
342 let resolve = PromiseWalker.completePromise
343 .bind(PromiseWalker, this, STATUS_RESOLVED);
344 let reject = PromiseWalker.completePromise
345 .bind(PromiseWalker, this, STATUS_REJECTED);
348 aExecutor.call(undefined, resolve, reject);
355 * Calls one of the provided functions as soon as this promise is either
356 * resolved or rejected. A new promise is returned, whose state evolves
357 * depending on this promise and the provided callback functions.
359 * The appropriate callback is always invoked after this method returns, even
360 * if this promise is already resolved or rejected. You can also call the
361 * "then" method multiple times on the same promise, and the callbacks will be
362 * invoked in the same order as they were registered.
365 * If the promise is resolved, this function is invoked with the
366 * resolution value of the promise as its only argument, and the
367 * outcome of the function determines the state of the new promise
368 * returned by the "then" method. In case this parameter is not a
369 * function (usually "null"), the new promise returned by the "then"
370 * method is resolved with the same value as the original promise.
373 * If the promise is rejected, this function is invoked with the
374 * rejection reason of the promise as its only argument, and the
375 * outcome of the function determines the state of the new promise
376 * returned by the "then" method. In case this parameter is not a
377 * function (usually left "undefined"), the new promise returned by the
378 * "then" method is rejected with the same reason as the original
381 * @return A new promise that is initially pending, then assumes a state that
382 * depends on the outcome of the invoked callback function:
383 * - If the callback returns a value that is not a promise, including
384 * "undefined", the new promise is resolved with this resolution
385 * value, even if the original promise was rejected.
386 * - If the callback throws an exception, the new promise is rejected
387 * with the exception as the rejection reason, even if the original
388 * promise was resolved.
389 * - If the callback returns a promise, the new promise will
390 * eventually assume the same state as the returned promise.
392 * @note If the aOnResolve callback throws an exception, the aOnReject
393 * callback is not invoked. You can register a rejection callback on
394 * the returned promise instead, to process any exception occurred in
395 * either of the callbacks registered on this promise.
397 Promise.prototype.then = function (aOnResolve, aOnReject)
399 let handler = new Handler(this, aOnResolve, aOnReject);
400 this[N_INTERNALS].handlers.push(handler);
402 // Ensure the handler is scheduled for processing if this promise is already
403 // resolved or rejected.
404 if (this[N_INTERNALS].status != STATUS_PENDING) {
406 // This promise is not the last in the chain anymore. Remove any watchdog.
407 if (this[N_INTERNALS].witness != null) {
408 let [id, witness] = this[N_INTERNALS].witness;
409 this[N_INTERNALS].witness = null;
411 PendingErrors.unregister(id);
414 PromiseWalker.schedulePromise(this);
417 return handler.nextPromise;
421 * Invokes `promise.then` with undefined for the resolve handler and a given
425 * The rejection handler.
427 * @return A new pending promise returned.
429 * @see Promise.prototype.then
431 Promise.prototype.catch = function (aOnReject)
433 return this.then(undefined, aOnReject);
437 * Creates a new pending promise and provides methods to resolve or reject it.
439 * @return A new object, containing the new promise in the "promise" property,
440 * and the methods to change its state in the "resolve" and "reject"
441 * properties. See the Deferred documentation for details.
443 Promise.defer = function ()
445 return new Deferred();
449 * Creates a new promise resolved with the specified value, or propagates the
450 * state of an existing promise.
453 * If this value is not a promise, including "undefined", it becomes
454 * the resolution value of the returned promise. If this value is a
455 * promise, then the returned promise will eventually assume the same
456 * state as the provided promise.
458 * @return A promise that can be pending, resolved, or rejected.
460 Promise.resolve = function (aValue)
462 if (aValue && typeof(aValue) == "function" && aValue.isAsyncFunction) {
464 "Cannot resolve a promise with an async function. " +
465 "You should either invoke the async function first " +
466 "or use 'Task.spawn' instead of 'Task.async' to start " +
467 "the Task and return its promise.");
470 if (aValue instanceof Promise) {
474 return new Promise((aResolve) => aResolve(aValue));
478 * Creates a new promise rejected with the specified reason.
481 * The rejection reason for the returned promise. Although the reason
482 * can be "undefined", it is generally an Error object, like in
483 * exception handling.
485 * @return A rejected promise.
487 * @note The aReason argument should not be a promise. Using a rejected
488 * promise for the value of aReason would make the rejection reason
489 * equal to the rejected promise itself, and not its rejection reason.
491 Promise.reject = function (aReason)
493 return new Promise((_, aReject) => aReject(aReason));
497 * Returns a promise that is resolved or rejected when all values are
498 * resolved or any is rejected.
501 * Iterable of promises that may be pending, resolved, or rejected. When
502 * all are resolved or any is rejected, the returned promise will be
503 * resolved or rejected as well.
505 * @return A new promise that is fulfilled when all values are resolved or
506 * that is rejected when any of the values are rejected. Its
507 * resolution value will be an array of all resolved values in the
508 * given order, or undefined if aValues is an empty array. The reject
509 * reason will be forwarded from the first promise in the list of
510 * given promises to be rejected.
512 Promise.all = function (aValues)
514 if (aValues == null || typeof(aValues["@@iterator"]) != "function") {
515 throw new Error("Promise.all() expects an iterable.");
518 return new Promise((resolve, reject) => {
519 let values = Array.isArray(aValues) ? aValues : [...aValues];
520 let countdown = values.length;
521 let resolutionValues = new Array(countdown);
524 resolve(resolutionValues);
528 function checkForCompletion(aValue, aIndex) {
529 resolutionValues[aIndex] = aValue;
530 if (--countdown === 0) {
531 resolve(resolutionValues);
535 for (let i = 0; i < values.length; i++) {
537 let value = values[i];
538 let resolver = val => checkForCompletion(val, index);
540 if (value && typeof(value.then) == "function") {
541 value.then(resolver, reject);
543 // Given value is not a promise, forward it as a resolution value.
551 * Returns a promise that is resolved or rejected when the first value is
552 * resolved or rejected, taking on the value or reason of that promise.
555 * Iterable of values or promises that may be pending, resolved, or
556 * rejected. When any is resolved or rejected, the returned promise will
557 * be resolved or rejected as to the given value or reason.
559 * @return A new promise that is fulfilled when any values are resolved or
560 * rejected. Its resolution value will be forwarded from the resolution
561 * value or rejection reason.
563 Promise.race = function (aValues)
565 if (aValues == null || typeof(aValues["@@iterator"]) != "function") {
566 throw new Error("Promise.race() expects an iterable.");
569 return new Promise((resolve, reject) => {
570 for (let value of aValues) {
571 Promise.resolve(value).then(resolve, reject);
576 Promise.Debugging = {
578 * Add an observer notified when an error is reported as uncaught.
580 * @param {function} observer A function notified when an error is
581 * reported as uncaught. Its arguments are
582 * {message, date, fileName, stack, lineNumber}
583 * All arguments are optional.
585 addUncaughtErrorObserver: function(observer) {
586 PendingErrors.addObserver(observer);
590 * Remove an observer added with addUncaughtErrorObserver
592 * @param {function} An observer registered with
593 * addUncaughtErrorObserver.
595 removeUncaughtErrorObserver: function(observer) {
596 PendingErrors.removeObserver(observer);
600 * Remove all the observers added with addUncaughtErrorObserver
602 clearUncaughtErrorObservers: function() {
603 PendingErrors.removeAllObservers();
607 * Force all pending errors to be reported immediately as uncaught.
608 * Note that this may cause some false positives.
610 flushUncaughtErrors: function() {
611 PendingErrors.flush();
614 Object.freeze(Promise.Debugging);
616 Object.freeze(Promise);
618 ////////////////////////////////////////////////////////////////////////////////
622 * This singleton object invokes the handlers registered on resolved and
623 * rejected promises, ensuring that processing is not recursive and is done in
624 * the same order as registration occurred on each promise.
626 * There is no guarantee on the order of execution of handlers registered on
627 * different promises.
629 this.PromiseWalker = {
631 * Singleton array of all the unprocessed handlers currently registered on
632 * resolved or rejected promises. Handlers are removed from the array as soon
633 * as they are processed.
638 * Called when a promise needs to change state to be resolved or rejected.
641 * Promise that needs to change state. If this is already resolved or
642 * rejected, this method has no effect.
644 * New desired status, either STATUS_RESOLVED or STATUS_REJECTED.
646 * Associated resolution value or rejection reason.
648 completePromise: function (aPromise, aStatus, aValue)
650 // Do nothing if the promise is already resolved or rejected.
651 if (aPromise[N_INTERNALS].status != STATUS_PENDING) {
655 // Resolving with another promise will cause this promise to eventually
656 // assume the state of the provided promise.
657 if (aStatus == STATUS_RESOLVED && aValue &&
658 typeof(aValue.then) == "function") {
659 aValue.then(this.completePromise.bind(this, aPromise, STATUS_RESOLVED),
660 this.completePromise.bind(this, aPromise, STATUS_REJECTED));
664 // Change the promise status and schedule our handlers for processing.
665 aPromise[N_INTERNALS].status = aStatus;
666 aPromise[N_INTERNALS].value = aValue;
667 if (aPromise[N_INTERNALS].handlers.length > 0) {
668 this.schedulePromise(aPromise);
669 } else if (aStatus == STATUS_REJECTED) {
670 // This is a rejection and the promise is the last in the chain.
671 // For the time being we therefore have an uncaught error.
672 let id = PendingErrors.register(aValue);
674 FinalizationWitnessService.make("promise-finalization-witness", id);
675 aPromise[N_INTERNALS].witness = [id, witness];
680 * Sets up the PromiseWalker loop to start on the next tick of the event loop
682 scheduleWalkerLoop: function()
684 this.walkerLoopScheduled = true;
685 Services.tm.currentThread.dispatch(this.walkerLoop,
686 Ci.nsIThread.DISPATCH_NORMAL);
690 * Schedules the resolution or rejection handlers registered on the provided
691 * promise for processing.
694 * Resolved or rejected promise whose handlers should be processed. It
695 * is expected that this promise has at least one handler to process.
697 schedulePromise: function (aPromise)
699 // Migrate the handlers from the provided promise to the global list.
700 for (let handler of aPromise[N_INTERNALS].handlers) {
701 this.handlers.push(handler);
703 aPromise[N_INTERNALS].handlers.length = 0;
705 // Schedule the walker loop on the next tick of the event loop.
706 if (!this.walkerLoopScheduled) {
707 this.scheduleWalkerLoop();
712 * Indicates whether the walker loop is currently scheduled for execution on
713 * the next tick of the event loop.
715 walkerLoopScheduled: false,
718 * Processes all the known handlers during this tick of the event loop. This
719 * eager processing is done to avoid unnecessarily exiting and re-entering the
720 * JavaScript context for each handler on a resolved or rejected promise.
722 * This function is called with "this" bound to the PromiseWalker object.
724 walkerLoop: function ()
726 // If there is more than one handler waiting, reschedule the walker loop
727 // immediately. Otherwise, use walkerLoopScheduled to tell schedulePromise()
728 // to reschedule the loop if it adds more handlers to the queue. This makes
729 // this walker resilient to the case where one handler does not return, but
730 // starts a nested event loop. In that case, the newly scheduled walker will
731 // take over. In the common case, the newly scheduled walker will be invoked
732 // after this one has returned, with no actual handler to process. This
733 // small overhead is required to make nested event loops work correctly, but
734 // occurs at most once per resolution chain, thus having only a minor
735 // impact on overall performance.
736 if (this.handlers.length > 1) {
737 this.scheduleWalkerLoop();
739 this.walkerLoopScheduled = false;
742 // Process all the known handlers eagerly.
743 while (this.handlers.length > 0) {
744 this.handlers.shift().process();
749 // Bind the function to the singleton once.
750 PromiseWalker.walkerLoop = PromiseWalker.walkerLoop.bind(PromiseWalker);
752 ////////////////////////////////////////////////////////////////////////////////
756 * Returned by "Promise.defer" to provide a new promise along with methods to
761 this.promise = new Promise((aResolve, aReject) => {
762 this.resolve = aResolve;
763 this.reject = aReject;
768 Deferred.prototype = {
770 * A newly created promise, initially in the pending state.
775 * Resolves the associated promise with the specified value, or propagates the
776 * state of an existing promise. If the associated promise has already been
777 * resolved or rejected, this method does nothing.
779 * This function is bound to its associated promise when "Promise.defer" is
780 * called, and can be called with any value of "this".
783 * If this value is not a promise, including "undefined", it becomes
784 * the resolution value of the associated promise. If this value is a
785 * promise, then the associated promise will eventually assume the same
786 * state as the provided promise.
788 * @note Calling this method with a pending promise as the aValue argument,
789 * and then calling it again with another value before the promise is
790 * resolved or rejected, has unspecified behavior and should be avoided.
795 * Rejects the associated promise with the specified reason. If the promise
796 * has already been resolved or rejected, this method does nothing.
798 * This function is bound to its associated promise when "Promise.defer" is
799 * called, and can be called with any value of "this".
802 * The rejection reason for the associated promise. Although the
803 * reason can be "undefined", it is generally an Error object, like in
804 * exception handling.
806 * @note The aReason argument should not generally be a promise. In fact,
807 * using a rejected promise for the value of aReason would make the
808 * rejection reason equal to the rejected promise itself, not to the
809 * rejection reason of the rejected promise.
814 ////////////////////////////////////////////////////////////////////////////////
818 * Handler registered on a promise by the "then" function.
820 function Handler(aThisPromise, aOnResolve, aOnReject)
822 this.thisPromise = aThisPromise;
823 this.onResolve = aOnResolve;
824 this.onReject = aOnReject;
825 this.nextPromise = new Promise(() => {});
828 Handler.prototype = {
830 * Promise on which the "then" method was called.
835 * Unmodified resolution handler provided to the "then" method.
840 * Unmodified rejection handler provided to the "then" method.
845 * New promise that will be returned by the "then" method.
850 * Called after thisPromise is resolved or rejected, invokes the appropriate
851 * callback and propagates the result to nextPromise.
855 // The state of this promise is propagated unless a handler is defined.
856 let nextStatus = this.thisPromise[N_INTERNALS].status;
857 let nextValue = this.thisPromise[N_INTERNALS].value;
860 // If a handler is defined for either resolution or rejection, invoke it
861 // to determine the state of the next promise, that will be resolved with
862 // the returned value, that can also be another promise.
863 if (nextStatus == STATUS_RESOLVED) {
864 if (typeof(this.onResolve) == "function") {
865 nextValue = this.onResolve.call(undefined, nextValue);
867 } else if (typeof(this.onReject) == "function") {
868 nextValue = this.onReject.call(undefined, nextValue);
869 nextStatus = STATUS_RESOLVED;
873 // An exception has occurred in the handler.
875 if (ex && typeof ex == "object" && "name" in ex &&
876 ERRORS_TO_REPORT.indexOf(ex.name) != -1) {
878 // We suspect that the exception is a programmer error, so we now
879 // display it using dump(). Note that we do not use Cu.reportError as
880 // we assume that this is a programming error, so we do not want end
881 // users to see it. Also, if the programmer handles errors correctly,
882 // they will either treat the error or log them somewhere.
884 dump("*************************\n");
885 dump("A coding exception was thrown in a Promise " +
886 ((nextStatus == STATUS_RESOLVED) ? "resolution":"rejection") +
888 dump("See https://developer.mozilla.org/Mozilla/JavaScript_code_modules/Promise.jsm/Promise\n\n");
889 dump("Full message: " + ex + "\n");
890 dump("Full stack: " + (("stack" in ex)?ex.stack:"not available") + "\n");
891 dump("*************************\n");
895 // Additionally, reject the next promise.
896 nextStatus = STATUS_REJECTED;
900 // Propagate the newly determined state to the next promise.
901 PromiseWalker.completePromise(this.nextPromise, nextStatus, nextValue);