// Copyright 2012 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. "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. // Status values: 0 = pending, +1 = resolved, -1 = rejected var promiseStatus = GLOBAL_PRIVATE("Promise#status"); var promiseValue = GLOBAL_PRIVATE("Promise#value"); var promiseOnResolve = GLOBAL_PRIVATE("Promise#onResolve"); var promiseOnReject = GLOBAL_PRIVATE("Promise#onReject"); var promiseRaw = GLOBAL_PRIVATE("Promise#raw"); function IsPromise(x) { return IS_SPEC_OBJECT(x) && %HasLocalProperty(x, promiseStatus); } function Promise(resolver) { if (resolver === promiseRaw) return; if (!%_IsConstructCall()) throw MakeTypeError('not_a_promise', [this]); if (typeof resolver !== 'function') throw MakeTypeError('resolver_not_a_function', [resolver]); var promise = PromiseInit(this); try { resolver(function(x) { PromiseResolve(promise, x) }, function(r) { PromiseReject(promise, r) }); } catch (e) { PromiseReject(promise, e); } } 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) { 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) } // For API. function PromiseNopResolver() {} function PromiseCreate() { return new Promise(PromiseNopResolver) } // 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.then(UNDEFINED, onReject); } function PromiseEnqueue(value, tasks) { GetMicrotaskQueue().push(function() { for (var i = 0; i < tasks.length; i += 2) { PromiseHandle(value, tasks[i], tasks[i + 1]) } }); %SetMicrotaskPending(true); } function PromiseHandle(value, handler, deferred) { try { var result = handler(value); if (result === deferred.promise) throw MakeTypeError('promise_cyclic', [result]); else if (IsPromise(result)) %_CallFunction(result, deferred.resolve, deferred.reject, PromiseChain); else deferred.resolve(result); } catch (exception) { var uncaught = false; var reject_queue = GET_PRIVATE(deferred.promise, promiseOnReject); if (reject_queue && reject_queue.length == 0) { // The deferred promise may get a reject handler attached later. // For now, we consider the exception to be (for the moment) uncaught. uncaught = true; } try { deferred.reject(exception); } catch (e) { // The reject handler can only throw for a custom deferred promise. // We consider the original exception to be uncaught. uncaught = true; } if (uncaught) %DebugPendingExceptionInPromise(exception, deferred.promise); } } // Multi-unwrapped chaining with thenable coercion. function PromiseThen(onResolve, onReject) { onResolve = IS_NULL_OR_UNDEFINED(onResolve) ? PromiseIdResolveHandler : onResolve; onReject = IS_NULL_OR_UNDEFINED(onReject) ? PromiseIdRejectHandler : onReject; var that = this; var constructor = this.constructor; return %_CallFunction( this, function(x) { x = PromiseCoerce(constructor, x); return x === that ? onReject(MakeTypeError('promise_cyclic', [x])) : IsPromise(x) ? x.then(onResolve, onReject) : onResolve(x); }, onReject, PromiseChain ); } PromiseCoerce.table = new $WeakMap; function PromiseCoerce(constructor, x) { if (!IsPromise(x) && IS_SPEC_OBJECT(x)) { var then; try { then = x.then; } catch(r) { var promise = %_CallFunction(constructor, r, PromiseRejected); PromiseCoerce.table.set(x, promise); return promise; } if (typeof then === 'function') { if (PromiseCoerce.table.has(x)) { return PromiseCoerce.table.get(x); } else { var deferred = %_CallFunction(constructor, PromiseDeferred); PromiseCoerce.table.set(x, deferred.promise); try { %_CallFunction(x, deferred.resolve, deferred.reject, then); } catch(r) { deferred.reject(r); } return deferred.promise; } } } return x; } // Combinators. function PromiseCast(x) { // TODO(rossberg): cannot do better until we support @@create. return IsPromise(x) ? x : new this(function(resolve) { resolve(x) }); } function PromiseAll(values) { var deferred = %_CallFunction(this, PromiseDeferred); var resolutions = []; if (!%_IsArray(values)) { deferred.reject(MakeTypeError('invalid_argument')); return deferred.promise; } try { var count = values.length; if (count === 0) { deferred.resolve(resolutions); } else { for (var i = 0; i < values.length; ++i) { this.resolve(values[i]).then( function(i, x) { resolutions[i] = x; if (--count === 0) deferred.resolve(resolutions); }.bind(UNDEFINED, i), // TODO(rossberg): use let loop once available function(r) { deferred.reject(r) } ); } } } catch (e) { deferred.reject(e) } return deferred.promise; } function PromiseOne(values) { var deferred = %_CallFunction(this, PromiseDeferred); if (!%_IsArray(values)) { deferred.reject(MakeTypeError('invalid_argument')); return deferred.promise; } try { for (var i = 0; i < values.length; ++i) { this.resolve(values[i]).then( function(x) { deferred.resolve(x) }, function(r) { deferred.reject(r) } ); } } catch (e) { deferred.reject(e) } return deferred.promise; } //------------------------------------------------------------------- function SetUpPromise() { %CheckIsBootstrapping(); %SetProperty(global, 'Promise', $Promise, DONT_ENUM); InstallFunctions($Promise, DONT_ENUM, [ "defer", PromiseDeferred, "accept", PromiseResolved, "reject", PromiseRejected, "all", PromiseAll, "race", PromiseOne, "resolve", PromiseCast ]); InstallFunctions($Promise.prototype, DONT_ENUM, [ "chain", PromiseChain, "then", PromiseThen, "catch", PromiseCatch ]); } SetUpPromise(); // Functions to expose promise details to the debugger. function GetPromiseStatus(promise) { return GET_PRIVATE(promise, promiseStatus); } function GetPromiseOnResolve(promise) { return GET_PRIVATE(promise, promiseOnResolve); } function GetPromiseOnReject(promise) { return GET_PRIVATE(promise, promiseOnReject); } function GetPromiseValue(promise) { return GET_PRIVATE(promise, promiseValue); }