// 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.
(function(global, utils, extrasUtils) {
"use strict";
%CheckIsBootstrapping();
// -------------------------------------------------------------------
// Imports
var InternalArray = utils.InternalArray;
var promiseAsyncStackIDSymbol =
utils.ImportNow("promise_async_stack_id_symbol");
var promiseHandledBySymbol =
utils.ImportNow("promise_handled_by_symbol");
var promiseForwardingHandlerSymbol =
utils.ImportNow("promise_forwarding_handler_symbol");
var promiseHasHandlerSymbol =
utils.ImportNow("promise_has_handler_symbol");
var promiseRejectReactionsSymbol =
utils.ImportNow("promise_reject_reactions_symbol");
var promiseFulfillReactionsSymbol =
utils.ImportNow("promise_fulfill_reactions_symbol");
var promiseDeferredReactionSymbol =
utils.ImportNow("promise_deferred_reaction_symbol");
var promiseHandledHintSymbol =
utils.ImportNow("promise_handled_hint_symbol");
var promiseRawSymbol = utils.ImportNow("promise_raw_symbol");
var promiseStateSymbol = utils.ImportNow("promise_state_symbol");
var promiseResultSymbol = utils.ImportNow("promise_result_symbol");
var SpeciesConstructor;
var speciesSymbol = utils.ImportNow("species_symbol");
var toStringTagSymbol = utils.ImportNow("to_string_tag_symbol");
var ObjectHasOwnProperty;
utils.Import(function(from) {
ObjectHasOwnProperty = from.ObjectHasOwnProperty;
SpeciesConstructor = from.SpeciesConstructor;
});
// -------------------------------------------------------------------
// [[PromiseState]] values:
// These values should be kept in sync with PromiseStatus in globals.h
const kPending = 0;
const kFulfilled = +1;
const kRejected = +2;
const kResolveCallback = 0;
const kRejectCallback = 1;
// ES#sec-promise-executor
// Promise ( executor )
var GlobalPromise = function Promise(executor) {
if (executor === promiseRawSymbol) {
return %_NewObject(GlobalPromise, new.target);
}
if (IS_UNDEFINED(new.target)) throw %make_type_error(kNotAPromise, this);
if (!IS_CALLABLE(executor)) {
throw %make_type_error(kResolverNotAFunction, executor);
}
var promise = PromiseInit(%_NewObject(GlobalPromise, new.target));
// Calling the reject function would be a new exception, so debugEvent = true
// TODO(gsathya): Remove container for callbacks when this is moved
// to CPP/TF.
var callbacks = %create_resolving_functions(promise, true);
var debug_is_active = DEBUG_IS_ACTIVE;
try {
if (debug_is_active) %DebugPushPromise(promise);
executor(callbacks[kResolveCallback], callbacks[kRejectCallback]);
} %catch (e) { // Natives syntax to mark this catch block.
%_Call(callbacks[kRejectCallback], UNDEFINED, e);
} finally {
if (debug_is_active) %DebugPopPromise();
}
return promise;
}
// Core functionality.
function PromiseSet(promise, status, value) {
SET_PRIVATE(promise, promiseStateSymbol, status);
SET_PRIVATE(promise, promiseResultSymbol, value);
// There are 3 possible states for the resolve, reject symbols when we add
// a new callback --
// 1) UNDEFINED -- This is the zero state where there is no callback
// registered. When we see this state, we directly attach the callbacks to
// the symbol.
// 2) !IS_ARRAY -- There is a single callback directly attached to the
// symbols. We need to create a new array to store additional callbacks.
// 3) IS_ARRAY -- There are multiple callbacks already registered,
// therefore we can just push the new callback to the existing array.
SET_PRIVATE(promise, promiseFulfillReactionsSymbol, UNDEFINED);
SET_PRIVATE(promise, promiseRejectReactionsSymbol, UNDEFINED);
// This symbol is used only when one deferred needs to be attached. When more
// than one deferred need to be attached the promise, we attach them directly
// to the promiseFulfillReactionsSymbol and promiseRejectReactionsSymbol and
// reset this back to UNDEFINED.
SET_PRIVATE(promise, promiseDeferredReactionSymbol, UNDEFINED);
return promise;
}
function PromiseCreateAndSet(status, value) {
var promise = new GlobalPromise(promiseRawSymbol);
// If debug is active, notify about the newly created promise first.
if (DEBUG_IS_ACTIVE) PromiseSet(promise, kPending, UNDEFINED);
return PromiseSet(promise, status, value);
}
function PromiseInit(promise) {
return PromiseSet(promise, kPending, UNDEFINED);
}
function PromiseHandle(value, handler, deferred) {
var debug_is_active = DEBUG_IS_ACTIVE;
try {
if (debug_is_active) %DebugPushPromise(deferred.promise);
var result = handler(value);
if (IS_UNDEFINED(deferred.resolve)) {
ResolvePromise(deferred.promise, result);
} else {
%_Call(deferred.resolve, UNDEFINED, result);
}
} %catch (exception) { // Natives syntax to mark this catch block.
try {
if (IS_UNDEFINED(deferred.reject)) {
// Pass false for debugEvent so .then chaining does not trigger
// redundant ExceptionEvents.
%PromiseReject(deferred.promise, exception, false);
PromiseSet(deferred.promise, kRejected, exception);
} else {
%_Call(deferred.reject, UNDEFINED, exception);
}
} catch (e) { }
} finally {
if (debug_is_active) %DebugPopPromise();
}
}
function PromiseDebugGetInfo(deferreds, status) {
var id, name, instrumenting = DEBUG_IS_ACTIVE;
if (instrumenting) {
// In an async function, reuse the existing stack related to the outer
// Promise. Otherwise, e.g. in a direct call to then, save a new stack.
// Promises with multiple reactions with one or more of them being async
// functions will not get a good stack trace, as async functions require
// different stacks from direct Promise use, but we save and restore a
// stack once for all reactions. TODO(littledan): Improve this case.
if (!IS_UNDEFINED(deferreds) &&
HAS_PRIVATE(deferreds.promise, promiseHandledBySymbol) &&
HAS_PRIVATE(GET_PRIVATE(deferreds.promise, promiseHandledBySymbol),
promiseAsyncStackIDSymbol)) {
id = GET_PRIVATE(GET_PRIVATE(deferreds.promise, promiseHandledBySymbol),
promiseAsyncStackIDSymbol);
name = "async function";
} else {
id = %DebugNextMicrotaskId();
name = status === kFulfilled ? "Promise.resolve" : "Promise.reject";
%DebugAsyncTaskEvent("enqueue", id, name);
}
}
return [id, name];
}
function PromiseAttachCallbacks(promise, deferred, onResolve, onReject) {
var maybeResolveCallbacks =
GET_PRIVATE(promise, promiseFulfillReactionsSymbol);
if (IS_UNDEFINED(maybeResolveCallbacks)) {
SET_PRIVATE(promise, promiseFulfillReactionsSymbol, onResolve);
SET_PRIVATE(promise, promiseRejectReactionsSymbol, onReject);
SET_PRIVATE(promise, promiseDeferredReactionSymbol, deferred);
} else if (!IS_ARRAY(maybeResolveCallbacks)) {
var resolveCallbacks = new InternalArray();
var rejectCallbacks = new InternalArray();
var existingDeferred = GET_PRIVATE(promise, promiseDeferredReactionSymbol);
resolveCallbacks.push(
maybeResolveCallbacks, existingDeferred, onResolve, deferred);
rejectCallbacks.push(GET_PRIVATE(promise, promiseRejectReactionsSymbol),
existingDeferred,
onReject,
deferred);
SET_PRIVATE(promise, promiseFulfillReactionsSymbol, resolveCallbacks);
SET_PRIVATE(promise, promiseRejectReactionsSymbol, rejectCallbacks);
SET_PRIVATE(promise, promiseDeferredReactionSymbol, UNDEFINED);
} else {
maybeResolveCallbacks.push(onResolve, deferred);
GET_PRIVATE(promise, promiseRejectReactionsSymbol).push(onReject, deferred);
}
}
function PromiseIdResolveHandler(x) { return x; }
function PromiseIdRejectHandler(r) { %_ReThrow(r); }
SET_PRIVATE(PromiseIdRejectHandler, promiseForwardingHandlerSymbol, true);
// -------------------------------------------------------------------
// Define exported functions.
// For bootstrapper.
// ES#sec-ispromise IsPromise ( x )
function IsPromise(x) {
return IS_RECEIVER(x) && HAS_DEFINED_PRIVATE(x, promiseStateSymbol);
}
function PromiseCreate() {
return PromiseInit(new GlobalPromise(promiseRawSymbol));
}
// ES#sec-promise-resolve-functions
// Promise Resolve Functions, steps 6-13
function ResolvePromise(promise, resolution) {
if (resolution === promise) {
var exception = %make_type_error(kPromiseCyclic, resolution);
%PromiseReject(promise, exception, true);
PromiseSet(promise, kRejected, exception);
return;
}
if (IS_RECEIVER(resolution)) {
// 25.4.1.3.2 steps 8-12
try {
var then = resolution.then;
} catch (e) {
%PromiseReject(promise, e, true);
PromiseSet(promise, kRejected, e);
return;
}
// Resolution is a native promise and if it's already resolved or
// rejected, shortcircuit the resolution procedure by directly
// reusing the value from the promise.
if (IsPromise(resolution) && then === PromiseThen) {
var thenableState = GET_PRIVATE(resolution, promiseStateSymbol);
if (thenableState === kFulfilled) {
// This goes inside the if-else to save one symbol lookup in
// the slow path.
var thenableValue = GET_PRIVATE(resolution, promiseResultSymbol);
%PromiseFulfill(promise, kFulfilled, thenableValue,
promiseFulfillReactionsSymbol);
PromiseSet(promise, kFulfilled, thenableValue);
SET_PRIVATE(promise, promiseHasHandlerSymbol, true);
return;
} else if (thenableState === kRejected) {
var thenableValue = GET_PRIVATE(resolution, promiseResultSymbol);
if (!HAS_DEFINED_PRIVATE(resolution, promiseHasHandlerSymbol)) {
// Promise has already been rejected, but had no handler.
// Revoke previously triggered reject event.
%PromiseRevokeReject(resolution);
}
// Don't cause a debug event as this case is forwarding a rejection
%PromiseReject(promise, thenableValue, false);
PromiseSet(promise, kRejected, thenableValue);
SET_PRIVATE(resolution, promiseHasHandlerSymbol, true);
return;
}
}
if (IS_CALLABLE(then)) {
if (DEBUG_IS_ACTIVE && IsPromise(resolution)) {
// Mark the dependency of the new promise on the resolution
SET_PRIVATE(resolution, promiseHandledBySymbol, promise);
}
%EnqueuePromiseResolveThenableJob(promise, resolution, then);
return;
}
}
%PromiseFulfill(promise, kFulfilled, resolution,
promiseFulfillReactionsSymbol);
PromiseSet(promise, kFulfilled, resolution);
}
// Only used by async-await.js
function RejectPromise(promise, reason, debugEvent) {
%PromiseReject(promise, reason, debugEvent);
PromiseSet(promise, kRejected, reason);
}
// Export to bindings
function DoRejectPromise(promise, reason) {
%PromiseReject(promise, reason, true);
PromiseSet(promise, kRejected, reason);
}
// ES#sec-newpromisecapability
// NewPromiseCapability ( C )
function NewPromiseCapability(C, debugEvent) {
if (C === GlobalPromise) {
// Optimized case, avoid extra closure.
var promise = PromiseCreate();
// TODO(gsathya): Remove container for callbacks when this is
// moved to CPP/TF.
var callbacks = %create_resolving_functions(promise, debugEvent);
return {
promise: promise,
resolve: callbacks[kResolveCallback],
reject: callbacks[kRejectCallback]
};
}
var result = {promise: UNDEFINED, resolve: UNDEFINED, reject: UNDEFINED };
result.promise = new C((resolve, reject) => {
if (!IS_UNDEFINED(result.resolve) || !IS_UNDEFINED(result.reject))
throw %make_type_error(kPromiseExecutorAlreadyInvoked);
result.resolve = resolve;
result.reject = reject;
});
if (!IS_CALLABLE(result.resolve) || !IS_CALLABLE(result.reject))
throw %make_type_error(kPromiseNonCallable);
return result;
}
// ES#sec-promise.reject
// Promise.reject ( x )
function PromiseReject(r) {
if (!IS_RECEIVER(this)) {
throw %make_type_error(kCalledOnNonObject, PromiseResolve);
}
if (this === GlobalPromise) {
// Optimized case, avoid extra closure.
var promise = PromiseCreateAndSet(kRejected, r);
// Trigger debug events if the debugger is on, as Promise.reject is
// equivalent to throwing an exception directly.
%PromiseRejectEventFromStack(promise, r);
return promise;
} else {
var promiseCapability = NewPromiseCapability(this, true);
%_Call(promiseCapability.reject, UNDEFINED, r);
return promiseCapability.promise;
}
}
function PerformPromiseThen(promise, onResolve, onReject, resultCapability) {
if (!IS_CALLABLE(onResolve)) onResolve = PromiseIdResolveHandler;
if (!IS_CALLABLE(onReject)) onReject = PromiseIdRejectHandler;
var status = GET_PRIVATE(promise, promiseStateSymbol);
switch (status) {
case kPending:
PromiseAttachCallbacks(promise, resultCapability, onResolve, onReject);
break;
case kFulfilled:
%EnqueuePromiseReactionJob(GET_PRIVATE(promise, promiseResultSymbol),
onResolve, resultCapability, kFulfilled);
break;
case kRejected:
if (!HAS_DEFINED_PRIVATE(promise, promiseHasHandlerSymbol)) {
// Promise has already been rejected, but had no handler.
// Revoke previously triggered reject event.
%PromiseRevokeReject(promise);
}
%EnqueuePromiseReactionJob(GET_PRIVATE(promise, promiseResultSymbol),
onReject, resultCapability, kRejected);
break;
}
// Mark this promise as having handler.
SET_PRIVATE(promise, promiseHasHandlerSymbol, true);
return resultCapability.promise;
}
// ES#sec-promise.prototype.then
// Promise.prototype.then ( onFulfilled, onRejected )
// Multi-unwrapped chaining with thenable coercion.
function PromiseThen(onResolve, onReject) {
var status = GET_PRIVATE(this, promiseStateSymbol);
if (IS_UNDEFINED(status)) {
throw %make_type_error(kNotAPromise, this);
}
var constructor = SpeciesConstructor(this, GlobalPromise);
var resultCapability;
// The resultCapability.promise is only ever fulfilled internally,
// so we don't need the closures to protect against accidentally
// calling them multiple times.
if (constructor === GlobalPromise) {
// TODO(gsathya): Combine this into NewPromiseCapability.
resultCapability = {
promise: PromiseCreate(),
resolve: UNDEFINED,
reject: UNDEFINED
};
} else {
// Pass false for debugEvent so .then chaining does not trigger
// redundant ExceptionEvents.
resultCapability = NewPromiseCapability(constructor, false);
}
return PerformPromiseThen(this, onResolve, onReject, resultCapability);
}
// ES#sec-promise.prototype.catch
// Promise.prototype.catch ( onRejected )
function PromiseCatch(onReject) {
return this.then(UNDEFINED, onReject);
}
// Combinators.
// ES#sec-promise.resolve
// Promise.resolve ( x )
function PromiseResolve(x) {
if (!IS_RECEIVER(this)) {
throw %make_type_error(kCalledOnNonObject, PromiseResolve);
}
if (IsPromise(x) && x.constructor === this) return x;
// Avoid creating resolving functions.
if (this === GlobalPromise) {
var promise = PromiseCreate();
ResolvePromise(promise, x);
return promise;
}
// debugEvent is not so meaningful here as it will be resolved
var promiseCapability = NewPromiseCapability(this, true);
%_Call(promiseCapability.resolve, UNDEFINED, x);
return promiseCapability.promise;
}
// ES#sec-promise.all
// Promise.all ( iterable )
function PromiseAll(iterable) {
if (!IS_RECEIVER(this)) {
throw %make_type_error(kCalledOnNonObject, "Promise.all");
}
// false debugEvent so that forwarding the rejection through all does not
// trigger redundant ExceptionEvents
var deferred = NewPromiseCapability(this, false);
var resolutions = new InternalArray();
var count;
// For catch prediction, don't treat the .then calls as handling it;
// instead, recurse outwards.
var instrumenting = DEBUG_IS_ACTIVE;
if (instrumenting) {
SET_PRIVATE(deferred.reject, promiseForwardingHandlerSymbol, true);
}
function CreateResolveElementFunction(index, values, promiseCapability) {
var alreadyCalled = false;
return (x) => {
if (alreadyCalled === true) return;
alreadyCalled = true;
values[index] = x;
if (--count === 0) {
var valuesArray = [];
%MoveArrayContents(values, valuesArray);
%_Call(promiseCapability.resolve, UNDEFINED, valuesArray);
}
};
}
try {
var i = 0;
count = 1;
for (var value of iterable) {
var nextPromise = this.resolve(value);
++count;
var throwawayPromise = nextPromise.then(
CreateResolveElementFunction(i, resolutions, deferred),
deferred.reject);
// For catch prediction, mark that rejections here are semantically
// handled by the combined Promise.
if (instrumenting && IsPromise(throwawayPromise)) {
SET_PRIVATE(throwawayPromise, promiseHandledBySymbol, deferred.promise);
}
++i;
}
// 6.d
if (--count === 0) {
var valuesArray = [];
%MoveArrayContents(resolutions, valuesArray);
%_Call(deferred.resolve, UNDEFINED, valuesArray);
}
} catch (e) {
%_Call(deferred.reject, UNDEFINED, e);
}
return deferred.promise;
}
// ES#sec-promise.race
// Promise.race ( iterable )
function PromiseRace(iterable) {
if (!IS_RECEIVER(this)) {
throw %make_type_error(kCalledOnNonObject, PromiseRace);
}
// false debugEvent so that forwarding the rejection through race does not
// trigger redundant ExceptionEvents
var deferred = NewPromiseCapability(this, false);
// For catch prediction, don't treat the .then calls as handling it;
// instead, recurse outwards.
var instrumenting = DEBUG_IS_ACTIVE;
if (instrumenting) {
SET_PRIVATE(deferred.reject, promiseForwardingHandlerSymbol, true);
}
try {
for (var value of iterable) {
var throwawayPromise = this.resolve(value).then(deferred.resolve,
deferred.reject);
// For catch prediction, mark that rejections here are semantically
// handled by the combined Promise.
if (instrumenting && IsPromise(throwawayPromise)) {
SET_PRIVATE(throwawayPromise, promiseHandledBySymbol, deferred.promise);
}
}
} catch (e) {
%_Call(deferred.reject, UNDEFINED, e);
}
return deferred.promise;
}
// Utility for debugger
function PromiseHasUserDefinedRejectHandlerCheck(handler, deferred) {
// Recurse to the forwarding Promise, if any. This may be due to
// - await reaction forwarding to the throwaway Promise, which has
// a dependency edge to the outer Promise.
// - PromiseIdResolveHandler forwarding to the output of .then
// - Promise.all/Promise.race forwarding to a throwaway Promise, which
// has a dependency edge to the generated outer Promise.
if (GET_PRIVATE(handler, promiseForwardingHandlerSymbol)) {
return PromiseHasUserDefinedRejectHandlerRecursive(deferred.promise);
}
// Otherwise, this is a real reject handler for the Promise
return true;
}
function PromiseHasUserDefinedRejectHandlerRecursive(promise) {
// If this promise was marked as being handled by a catch block
// in an async function, then it has a user-defined reject handler.
if (GET_PRIVATE(promise, promiseHandledHintSymbol)) return true;
// If this Promise is subsumed by another Promise (a Promise resolved
// with another Promise, or an intermediate, hidden, throwaway Promise
// within async/await), then recurse on the outer Promise.
// In this case, the dependency is one possible way that the Promise
// could be resolved, so it does not subsume the other following cases.
var outerPromise = GET_PRIVATE(promise, promiseHandledBySymbol);
if (outerPromise &&
PromiseHasUserDefinedRejectHandlerRecursive(outerPromise)) {
return true;
}
var queue = GET_PRIVATE(promise, promiseRejectReactionsSymbol);
var deferred = GET_PRIVATE(promise, promiseDeferredReactionSymbol);
if (IS_UNDEFINED(queue)) return false;
if (!IS_ARRAY(queue)) {
return PromiseHasUserDefinedRejectHandlerCheck(queue, deferred);
}
for (var i = 0; i < queue.length; i += 2) {
if (PromiseHasUserDefinedRejectHandlerCheck(queue[i], queue[i + 1])) {
return true;
}
}
return false;
}
// Return whether the promise will be handled by a user-defined reject
// handler somewhere down the promise chain. For this, we do a depth-first
// search for a reject handler that's not the default PromiseIdRejectHandler.
// This function also traverses dependencies of one Promise on another,
// set up through async/await and Promises resolved with Promises.
function PromiseHasUserDefinedRejectHandler() {
return PromiseHasUserDefinedRejectHandlerRecursive(this);
};
function MarkPromiseAsHandled(promise) {
SET_PRIVATE(promise, promiseHasHandlerSymbol, true);
}
function PromiseSpecies() {
return this;
}
// -------------------------------------------------------------------
// Install exported functions.
%AddNamedProperty(global, 'Promise', GlobalPromise, DONT_ENUM);
%AddNamedProperty(GlobalPromise.prototype, toStringTagSymbol, "Promise",
DONT_ENUM | READ_ONLY);
utils.InstallFunctions(GlobalPromise, DONT_ENUM, [
"reject", PromiseReject,
"all", PromiseAll,
"race", PromiseRace,
"resolve", PromiseResolve
]);
utils.InstallGetter(GlobalPromise, speciesSymbol, PromiseSpecies);
utils.InstallFunctions(GlobalPromise.prototype, DONT_ENUM, [
"then", PromiseThen,
"catch", PromiseCatch
]);
%InstallToContext([
"promise_catch", PromiseCatch,
"promise_create", PromiseCreate,
"promise_has_user_defined_reject_handler", PromiseHasUserDefinedRejectHandler,
"promise_reject", DoRejectPromise,
// TODO(gsathya): Remove this once we update the promise builtin.
"promise_internal_reject", RejectPromise,
"promise_resolve", ResolvePromise,
"promise_then", PromiseThen,
"promise_handle", PromiseHandle,
"promise_debug_get_info", PromiseDebugGetInfo
]);
// This allows extras to create promises quickly without building extra
// resolve/reject closures, and allows them to later resolve and reject any
// promise without having to hold on to those closures forever.
utils.InstallFunctions(extrasUtils, 0, [
"createPromise", PromiseCreate,
"resolvePromise", ResolvePromise,
"rejectPromise", DoRejectPromise,
"markPromiseAsHandled", MarkPromiseAsHandled
]);
utils.Export(function(to) {
to.IsPromise = IsPromise;
to.PromiseCreate = PromiseCreate;
to.PromiseThen = PromiseThen;
to.GlobalPromise = GlobalPromise;
to.NewPromiseCapability = NewPromiseCapability;
to.PerformPromiseThen = PerformPromiseThen;
to.ResolvePromise = ResolvePromise;
to.RejectPromise = RejectPromise;
});
})