1// Copyright 2012 the V8 project authors. All rights reserved.
2// Redistribution and use in source and binary forms, with or without
3// modification, are permitted provided that the following conditions are
4// met:
5//
6//     * Redistributions of source code must retain the above copyright
7//       notice, this list of conditions and the following disclaimer.
8//     * Redistributions in binary form must reproduce the above
9//       copyright notice, this list of conditions and the following
10//       disclaimer in the documentation and/or other materials provided
11//       with the distribution.
12//     * Neither the name of Google Inc. nor the names of its
13//       contributors may be used to endorse or promote products derived
14//       from this software without specific prior written permission.
15//
16// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
28
29"use strict";
30
31// This file relies on the fact that the following declaration has been made
32// in runtime.js:
33// var $Object = global.Object
34// var $WeakMap = global.WeakMap
35
36
37var $Promise = Promise;
38
39
40//-------------------------------------------------------------------
41
42// Core functionality.
43
44// Event queue format: [(value, [(handler, deferred)*])*]
45// I.e., a list of value/tasks pairs, where the value is a resolution value or
46// rejection reason, and the tasks are a respective list of handler/deferred
47// pairs waiting for notification of this value. Each handler is an onResolve or
48// onReject function provided to the same call of 'chain' that produced the
49// associated deferred.
50var promiseEvents = new InternalArray;
51
52// Status values: 0 = pending, +1 = resolved, -1 = rejected
53var promiseStatus = NEW_PRIVATE("Promise#status");
54var promiseValue = NEW_PRIVATE("Promise#value");
55var promiseOnResolve = NEW_PRIVATE("Promise#onResolve");
56var promiseOnReject = NEW_PRIVATE("Promise#onReject");
57var promiseRaw = NEW_PRIVATE("Promise#raw");
58
59function IsPromise(x) {
60  return IS_SPEC_OBJECT(x) && %HasLocalProperty(x, promiseStatus);
61}
62
63function Promise(resolver) {
64  if (resolver === promiseRaw) return;
65  var promise = PromiseInit(this);
66  resolver(function(x) { PromiseResolve(promise, x) },
67           function(r) { PromiseReject(promise, r) });
68  // TODO(rossberg): current draft makes exception from this call asynchronous,
69  // but that's probably a mistake.
70}
71
72function PromiseSet(promise, status, value, onResolve, onReject) {
73  SET_PRIVATE(promise, promiseStatus, status);
74  SET_PRIVATE(promise, promiseValue, value);
75  SET_PRIVATE(promise, promiseOnResolve, onResolve);
76  SET_PRIVATE(promise, promiseOnReject, onReject);
77  return promise;
78}
79
80function PromiseInit(promise) {
81  return PromiseSet(promise, 0, UNDEFINED, new InternalArray, new InternalArray)
82}
83
84function PromiseDone(promise, status, value, promiseQueue) {
85  if (GET_PRIVATE(promise, promiseStatus) !== 0) return;
86  PromiseEnqueue(value, GET_PRIVATE(promise, promiseQueue));
87  PromiseSet(promise, status, value);
88}
89
90function PromiseResolve(promise, x) {
91  PromiseDone(promise, +1, x, promiseOnResolve)
92}
93
94function PromiseReject(promise, r) {
95  PromiseDone(promise, -1, r, promiseOnReject)
96}
97
98
99// Convenience.
100
101function PromiseDeferred() {
102  if (this === $Promise) {
103    // Optimized case, avoid extra closure.
104    var promise = PromiseInit(new Promise(promiseRaw));
105    return {
106      promise: promise,
107      resolve: function(x) { PromiseResolve(promise, x) },
108      reject: function(r) { PromiseReject(promise, r) }
109    };
110  } else {
111    var result = {};
112    result.promise = new this(function(resolve, reject) {
113      result.resolve = resolve;
114      result.reject = reject;
115    })
116    return result;
117  }
118}
119
120function PromiseResolved(x) {
121  if (this === $Promise) {
122    // Optimized case, avoid extra closure.
123    return PromiseSet(new Promise(promiseRaw), +1, x);
124  } else {
125    return new this(function(resolve, reject) { resolve(x) });
126  }
127}
128
129function PromiseRejected(r) {
130  if (this === $Promise) {
131    // Optimized case, avoid extra closure.
132    return PromiseSet(new Promise(promiseRaw), -1, r);
133  } else {
134    return new this(function(resolve, reject) { reject(r) });
135  }
136}
137
138
139// Simple chaining.
140
141function PromiseIdResolveHandler(x) { return x }
142function PromiseIdRejectHandler(r) { throw r }
143
144function PromiseChain(onResolve, onReject) {  // a.k.a.  flatMap
145  onResolve = IS_UNDEFINED(onResolve) ? PromiseIdResolveHandler : onResolve;
146  onReject = IS_UNDEFINED(onReject) ? PromiseIdRejectHandler : onReject;
147  var deferred = %_CallFunction(this.constructor, PromiseDeferred);
148  switch (GET_PRIVATE(this, promiseStatus)) {
149    case UNDEFINED:
150      throw MakeTypeError('not_a_promise', [this]);
151    case 0:  // Pending
152      GET_PRIVATE(this, promiseOnResolve).push(onResolve, deferred);
153      GET_PRIVATE(this, promiseOnReject).push(onReject, deferred);
154      break;
155    case +1:  // Resolved
156      PromiseEnqueue(GET_PRIVATE(this, promiseValue), [onResolve, deferred]);
157      break;
158    case -1:  // Rejected
159      PromiseEnqueue(GET_PRIVATE(this, promiseValue), [onReject, deferred]);
160      break;
161  }
162  return deferred.promise;
163}
164
165function PromiseCatch(onReject) {
166  return this.chain(UNDEFINED, onReject);
167}
168
169function PromiseEnqueue(value, tasks) {
170  promiseEvents.push(value, tasks);
171  %SetMicrotaskPending(true);
172}
173
174function PromiseMicrotaskRunner() {
175  var events = promiseEvents;
176  if (events.length > 0) {
177    promiseEvents = new InternalArray;
178    for (var i = 0; i < events.length; i += 2) {
179      var value = events[i];
180      var tasks = events[i + 1];
181      for (var j = 0; j < tasks.length; j += 2) {
182        var handler = tasks[j];
183        var deferred = tasks[j + 1];
184        try {
185          var result = handler(value);
186          if (result === deferred.promise)
187            throw MakeTypeError('promise_cyclic', [result]);
188          else if (IsPromise(result))
189            result.chain(deferred.resolve, deferred.reject);
190          else
191            deferred.resolve(result);
192        } catch(e) {
193          // TODO(rossberg): perhaps log uncaught exceptions below.
194          try { deferred.reject(e) } catch(e) {}
195        }
196      }
197    }
198  }
199}
200RunMicrotasks.runners.push(PromiseMicrotaskRunner);
201
202
203// Multi-unwrapped chaining with thenable coercion.
204
205function PromiseThen(onResolve, onReject) {
206  onResolve = IS_UNDEFINED(onResolve) ? PromiseIdResolveHandler : onResolve;
207  var that = this;
208  var constructor = this.constructor;
209  return this.chain(
210    function(x) {
211      x = PromiseCoerce(constructor, x);
212      return x === that ? onReject(MakeTypeError('promise_cyclic', [x])) :
213             IsPromise(x) ? x.then(onResolve, onReject) : onResolve(x);
214    },
215    onReject
216  );
217}
218
219PromiseCoerce.table = new $WeakMap;
220
221function PromiseCoerce(constructor, x) {
222  var then;
223  if (IsPromise(x)) {
224    return x;
225  } else if (!IS_NULL_OR_UNDEFINED(x) && %IsCallable(then = x.then)) {
226    if (PromiseCoerce.table.has(x)) {
227      return PromiseCoerce.table.get(x);
228    } else {
229      var deferred = constructor.deferred();
230      PromiseCoerce.table.set(x, deferred.promise);
231      try {
232        %_CallFunction(x, deferred.resolve, deferred.reject, then);
233      } catch(e) {
234        deferred.reject(e);
235      }
236      return deferred.promise;
237    }
238  } else {
239    return x;
240  }
241}
242
243
244// Combinators.
245
246function PromiseCast(x) {
247  // TODO(rossberg): cannot do better until we support @@create.
248  return IsPromise(x) ? x : this.resolved(x);
249}
250
251function PromiseAll(values) {
252  var deferred = this.deferred();
253  var resolutions = [];
254  var count = values.length;
255  if (count === 0) {
256    deferred.resolve(resolutions);
257  } else {
258    for (var i = 0; i < values.length; ++i) {
259      this.cast(values[i]).chain(
260        function(i, x) {
261          resolutions[i] = x;
262          if (--count === 0) deferred.resolve(resolutions);
263        }.bind(UNDEFINED, i),  // TODO(rossberg): use let loop once available
264        function(r) {
265          if (count > 0) { count = 0; deferred.reject(r) }
266        }
267      );
268    }
269  }
270  return deferred.promise;
271}
272
273function PromiseOne(values) {  // a.k.a. race
274  var deferred = this.deferred();
275  var done = false;
276  for (var i = 0; i < values.length; ++i) {
277    this.cast(values[i]).chain(
278      function(x) { if (!done) { done = true; deferred.resolve(x) } },
279      function(r) { if (!done) { done = true; deferred.reject(r) } }
280    );
281  }
282  return deferred.promise;
283}
284
285//-------------------------------------------------------------------
286
287function SetUpPromise() {
288  %CheckIsBootstrapping()
289  global.Promise = $Promise;
290  InstallFunctions($Promise, DONT_ENUM, [
291    "deferred", PromiseDeferred,
292    "resolved", PromiseResolved,
293    "rejected", PromiseRejected,
294    "all", PromiseAll,
295    "one", PromiseOne,
296    "cast", PromiseCast
297  ]);
298  InstallFunctions($Promise.prototype, DONT_ENUM, [
299    "chain", PromiseChain,
300    "then", PromiseThen,
301    "catch", PromiseCatch
302  ]);
303}
304
305SetUpPromise();
306