blob: 30f4f07b4b7b21c1c83b0c6e590eeb8f4c2803d6 [file] [log] [blame]
// 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();