// Copyright 2012 the V8 project authors. All rights reserved. // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following // disclaimer in the documentation and/or other materials provided // with the distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived // from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. "use strict"; // This file relies on the fact that the following declaration has been made // in runtime.js: // var $Object = global.Object // var $WeakMap = global.WeakMap var $Promise = Promise; //------------------------------------------------------------------- // Core functionality. // Event queue format: [(value, [(handler, deferred)*])*] // I.e., a list of value/tasks pairs, where the value is a resolution value or // rejection reason, and the tasks are a respective list of handler/deferred // pairs waiting for notification of this value. Each handler is an onResolve or // onReject function provided to the same call of 'chain' that produced the // associated deferred. var promiseEvents = new InternalArray; // Status values: 0 = pending, +1 = resolved, -1 = rejected var promiseStatus = NEW_PRIVATE("Promise#status"); var promiseValue = NEW_PRIVATE("Promise#value"); var promiseOnResolve = NEW_PRIVATE("Promise#onResolve"); var promiseOnReject = NEW_PRIVATE("Promise#onReject"); var promiseRaw = NEW_PRIVATE("Promise#raw"); function IsPromise(x) { return IS_SPEC_OBJECT(x) && %HasLocalProperty(x, promiseStatus); } function Promise(resolver) { if (resolver === promiseRaw) return; var promise = PromiseInit(this); resolver(function(x) { PromiseResolve(promise, x) }, function(r) { PromiseReject(promise, r) }); // TODO(rossberg): current draft makes exception from this call asynchronous, // but that's probably a mistake. } function PromiseSet(promise, status, value, onResolve, onReject) { SET_PRIVATE(promise, promiseStatus, status); SET_PRIVATE(promise, promiseValue, value); SET_PRIVATE(promise, promiseOnResolve, onResolve); SET_PRIVATE(promise, promiseOnReject, onReject); return promise; } function PromiseInit(promise) { return PromiseSet(promise, 0, UNDEFINED, new InternalArray, new InternalArray) } function PromiseDone(promise, status, value, promiseQueue) { if (GET_PRIVATE(promise, promiseStatus) !== 0) return; PromiseEnqueue(value, GET_PRIVATE(promise, promiseQueue)); PromiseSet(promise, status, value); } function PromiseResolve(promise, x) { PromiseDone(promise, +1, x, promiseOnResolve) } function PromiseReject(promise, r) { PromiseDone(promise, -1, r, promiseOnReject) } // Convenience. function PromiseDeferred() { if (this === $Promise) { // Optimized case, avoid extra closure. var promise = PromiseInit(new Promise(promiseRaw)); return { promise: promise, resolve: function(x) { PromiseResolve(promise, x) }, reject: function(r) { PromiseReject(promise, r) } }; } else { var result = {}; result.promise = new this(function(resolve, reject) { result.resolve = resolve; result.reject = reject; }) return result; } } function PromiseResolved(x) { if (this === $Promise) { // Optimized case, avoid extra closure. return PromiseSet(new Promise(promiseRaw), +1, x); } else { return new this(function(resolve, reject) { resolve(x) }); } } function PromiseRejected(r) { if (this === $Promise) { // Optimized case, avoid extra closure. return PromiseSet(new Promise(promiseRaw), -1, r); } else { return new this(function(resolve, reject) { reject(r) }); } } // Simple chaining. function PromiseIdResolveHandler(x) { return x } function PromiseIdRejectHandler(r) { throw r } function PromiseChain(onResolve, onReject) { // a.k.a. flatMap onResolve = IS_UNDEFINED(onResolve) ? PromiseIdResolveHandler : onResolve; onReject = IS_UNDEFINED(onReject) ? PromiseIdRejectHandler : onReject; var deferred = %_CallFunction(this.constructor, PromiseDeferred); switch (GET_PRIVATE(this, promiseStatus)) { case UNDEFINED: throw MakeTypeError('not_a_promise', [this]); case 0: // Pending GET_PRIVATE(this, promiseOnResolve).push(onResolve, deferred); GET_PRIVATE(this, promiseOnReject).push(onReject, deferred); break; case +1: // Resolved PromiseEnqueue(GET_PRIVATE(this, promiseValue), [onResolve, deferred]); break; case -1: // Rejected PromiseEnqueue(GET_PRIVATE(this, promiseValue), [onReject, deferred]); break; } return deferred.promise; } function PromiseCatch(onReject) { return this.chain(UNDEFINED, onReject); } function PromiseEnqueue(value, tasks) { promiseEvents.push(value, tasks); %SetMicrotaskPending(true); } function PromiseMicrotaskRunner() { var events = promiseEvents; if (events.length > 0) { promiseEvents = new InternalArray; for (var i = 0; i < events.length; i += 2) { var value = events[i]; var tasks = events[i + 1]; for (var j = 0; j < tasks.length; j += 2) { var handler = tasks[j]; var deferred = tasks[j + 1]; try { var result = handler(value); if (result === deferred.promise) throw MakeTypeError('promise_cyclic', [result]); else if (IsPromise(result)) result.chain(deferred.resolve, deferred.reject); else deferred.resolve(result); } catch(e) { // TODO(rossberg): perhaps log uncaught exceptions below. try { deferred.reject(e) } catch(e) {} } } } } } RunMicrotasks.runners.push(PromiseMicrotaskRunner); // Multi-unwrapped chaining with thenable coercion. function PromiseThen(onResolve, onReject) { onResolve = IS_UNDEFINED(onResolve) ? PromiseIdResolveHandler : onResolve; var that = this; var constructor = this.constructor; return this.chain( function(x) { x = PromiseCoerce(constructor, x); return x === that ? onReject(MakeTypeError('promise_cyclic', [x])) : IsPromise(x) ? x.then(onResolve, onReject) : onResolve(x); }, onReject ); } PromiseCoerce.table = new $WeakMap; function PromiseCoerce(constructor, x) { var then; if (IsPromise(x)) { return x; } else if (!IS_NULL_OR_UNDEFINED(x) && %IsCallable(then = x.then)) { if (PromiseCoerce.table.has(x)) { return PromiseCoerce.table.get(x); } else { var deferred = constructor.deferred(); PromiseCoerce.table.set(x, deferred.promise); try { %_CallFunction(x, deferred.resolve, deferred.reject, then); } catch(e) { deferred.reject(e); } return deferred.promise; } } else { return x; } } // Combinators. function PromiseCast(x) { // TODO(rossberg): cannot do better until we support @@create. return IsPromise(x) ? x : this.resolved(x); } function PromiseAll(values) { var deferred = this.deferred(); var resolutions = []; var count = values.length; if (count === 0) { deferred.resolve(resolutions); } else { for (var i = 0; i < values.length; ++i) { this.cast(values[i]).chain( function(i, x) { resolutions[i] = x; if (--count === 0) deferred.resolve(resolutions); }.bind(UNDEFINED, i), // TODO(rossberg): use let loop once available function(r) { if (count > 0) { count = 0; deferred.reject(r) } } ); } } return deferred.promise; } function PromiseOne(values) { // a.k.a. race var deferred = this.deferred(); var done = false; for (var i = 0; i < values.length; ++i) { this.cast(values[i]).chain( function(x) { if (!done) { done = true; deferred.resolve(x) } }, function(r) { if (!done) { done = true; deferred.reject(r) } } ); } return deferred.promise; } //------------------------------------------------------------------- function SetUpPromise() { %CheckIsBootstrapping() global.Promise = $Promise; InstallFunctions($Promise, DONT_ENUM, [ "deferred", PromiseDeferred, "resolved", PromiseResolved, "rejected", PromiseRejected, "all", PromiseAll, "one", PromiseOne, "cast", PromiseCast ]); InstallFunctions($Promise.prototype, DONT_ENUM, [ "chain", PromiseChain, "then", PromiseThen, "catch", PromiseCatch ]); } SetUpPromise();