blob: 2d8314a42fa5d8d5b0ec05638efec2d9a5ccbf03 [file] [log] [blame]
// 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
// For bootstrapper.
var IsPromise;
var PromiseCreate;
var PromiseResolve;
var PromiseReject;
var PromiseChain;
var PromiseCatch;
var PromiseThen;
var PromiseHasRejectHandler;
// mirror-debugger.js currently uses builtins.promiseStatus. It would be nice
// if we could move these property names into the closure below.
// TODO(jkummerow/rossberg/yangguo): Find a better solution.
// 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");
var promiseDebug = GLOBAL_PRIVATE("Promise#debug");
var lastMicrotaskId = 0;
(function() {
var $Promise = function Promise(resolver) {
if (resolver === promiseRaw) return;
if (!%_IsConstructCall()) throw MakeTypeError('not_a_promise', [this]);
if (!IS_SPEC_FUNCTION(resolver))
throw MakeTypeError('resolver_not_a_function', [resolver]);
var promise = PromiseInit(this);
try {
%DebugPushPromise(promise);
resolver(function(x) { PromiseResolve(promise, x) },
function(r) { PromiseReject(promise, r) });
} catch (e) {
PromiseReject(promise, e);
} finally {
%DebugPopPromise();
}
}
// Core functionality.
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);
if (DEBUG_IS_ACTIVE) {
%DebugPromiseEvent({ promise: promise, status: status, value: value });
}
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), status);
PromiseSet(promise, status, value);
}
}
function PromiseCoerce(constructor, x) {
if (!IsPromise(x) && IS_SPEC_OBJECT(x)) {
var then;
try {
then = x.then;
} catch(r) {
return %_CallFunction(constructor, r, PromiseRejected);
}
if (IS_SPEC_FUNCTION(then)) {
var deferred = %_CallFunction(constructor, PromiseDeferred);
try {
%_CallFunction(x, deferred.resolve, deferred.reject, then);
} catch(r) {
deferred.reject(r);
}
return deferred.promise;
}
}
return x;
}
function PromiseHandle(value, handler, deferred) {
try {
%DebugPushPromise(deferred.promise);
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) {
try { deferred.reject(exception); } catch (e) { }
} finally {
%DebugPopPromise();
}
}
function PromiseEnqueue(value, tasks, status) {
var id, name, instrumenting = DEBUG_IS_ACTIVE;
%EnqueueMicrotask(function() {
if (instrumenting) {
%DebugAsyncTaskEvent({ type: "willHandle", id: id, name: name });
}
for (var i = 0; i < tasks.length; i += 2) {
PromiseHandle(value, tasks[i], tasks[i + 1])
}
if (instrumenting) {
%DebugAsyncTaskEvent({ type: "didHandle", id: id, name: name });
}
});
if (instrumenting) {
id = ++lastMicrotaskId;
name = status > 0 ? "Promise.resolve" : "Promise.reject";
%DebugAsyncTaskEvent({ type: "enqueue", id: id, name: name });
}
}
function PromiseIdResolveHandler(x) { return x }
function PromiseIdRejectHandler(r) { throw r }
function PromiseNopResolver() {}
// -------------------------------------------------------------------
// Define exported functions.
// For bootstrapper.
IsPromise = function IsPromise(x) {
return IS_SPEC_OBJECT(x) && HAS_PRIVATE(x, promiseStatus);
}
PromiseCreate = function PromiseCreate() {
return new $Promise(PromiseNopResolver)
}
PromiseResolve = function PromiseResolve(promise, x) {
PromiseDone(promise, +1, x, promiseOnResolve)
}
PromiseReject = function PromiseReject(promise, r) {
// Check promise status to confirm that this reject has an effect.
// Check promiseDebug property to avoid duplicate event.
if (DEBUG_IS_ACTIVE &&
GET_PRIVATE(promise, promiseStatus) == 0 &&
!HAS_PRIVATE(promise, promiseDebug)) {
%DebugPromiseRejectEvent(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.
PromiseChain = 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],
+1);
break;
case -1: // Rejected
PromiseEnqueue(GET_PRIVATE(this, promiseValue),
[onReject, deferred],
-1);
break;
}
if (DEBUG_IS_ACTIVE) {
%DebugPromiseEvent({ promise: deferred.promise, parentPromise: this });
}
return deferred.promise;
}
PromiseCatch = function PromiseCatch(onReject) {
return this.then(UNDEFINED, onReject);
}
// Multi-unwrapped chaining with thenable coercion.
PromiseThen = function PromiseThen(onResolve, onReject) {
onResolve = IS_SPEC_FUNCTION(onResolve) ? onResolve
: PromiseIdResolveHandler;
onReject = IS_SPEC_FUNCTION(onReject) ? onReject
: PromiseIdRejectHandler;
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
);
}
// 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() {
// Nested scope to get closure over current i (and avoid .bind).
// TODO(rossberg): Use for-let instead once available.
var i_captured = i;
return function(x) {
resolutions[i_captured] = x;
if (--count === 0) deferred.resolve(resolutions);
};
})(),
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;
}
// Utility for debugger
PromiseHasRejectHandler = function PromiseHasRejectHandler() {
// Mark promise as already having triggered a reject event.
SET_PRIVATE(this, promiseDebug, true);
var queue = GET_PRIVATE(this, promiseOnReject);
return !IS_UNDEFINED(queue) && queue.length > 0;
};
// -------------------------------------------------------------------
// Install exported functions.
%CheckIsBootstrapping();
%AddNamedProperty(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
]);
})();