1// Copyright 2012 the V8 project authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5"use strict";
6
7// This file relies on the fact that the following declaration has been made
8// in runtime.js:
9// var $Object = global.Object
10// var $WeakMap = global.WeakMap
11
12// For bootstrapper.
13
14var IsPromise;
15var PromiseCreate;
16var PromiseResolve;
17var PromiseReject;
18var PromiseChain;
19var PromiseCatch;
20var PromiseThen;
21var PromiseHasRejectHandler;
22
23// mirror-debugger.js currently uses builtins.promiseStatus. It would be nice
24// if we could move these property names into the closure below.
25// TODO(jkummerow/rossberg/yangguo): Find a better solution.
26
27// Status values: 0 = pending, +1 = resolved, -1 = rejected
28var promiseStatus = GLOBAL_PRIVATE("Promise#status");
29var promiseValue = GLOBAL_PRIVATE("Promise#value");
30var promiseOnResolve = GLOBAL_PRIVATE("Promise#onResolve");
31var promiseOnReject = GLOBAL_PRIVATE("Promise#onReject");
32var promiseRaw = GLOBAL_PRIVATE("Promise#raw");
33var promiseDebug = GLOBAL_PRIVATE("Promise#debug");
34var lastMicrotaskId = 0;
35
36(function() {
37
38  var $Promise = function Promise(resolver) {
39    if (resolver === promiseRaw) return;
40    if (!%_IsConstructCall()) throw MakeTypeError('not_a_promise', [this]);
41    if (!IS_SPEC_FUNCTION(resolver))
42      throw MakeTypeError('resolver_not_a_function', [resolver]);
43    var promise = PromiseInit(this);
44    try {
45      %DebugPushPromise(promise);
46      resolver(function(x) { PromiseResolve(promise, x) },
47               function(r) { PromiseReject(promise, r) });
48    } catch (e) {
49      PromiseReject(promise, e);
50    } finally {
51      %DebugPopPromise();
52    }
53  }
54
55  // Core functionality.
56
57  function PromiseSet(promise, status, value, onResolve, onReject) {
58    SET_PRIVATE(promise, promiseStatus, status);
59    SET_PRIVATE(promise, promiseValue, value);
60    SET_PRIVATE(promise, promiseOnResolve, onResolve);
61    SET_PRIVATE(promise, promiseOnReject, onReject);
62    if (DEBUG_IS_ACTIVE) {
63      %DebugPromiseEvent({ promise: promise, status: status, value: value });
64    }
65    return promise;
66  }
67
68  function PromiseInit(promise) {
69    return PromiseSet(
70        promise, 0, UNDEFINED, new InternalArray, new InternalArray)
71  }
72
73  function PromiseDone(promise, status, value, promiseQueue) {
74    if (GET_PRIVATE(promise, promiseStatus) === 0) {
75      PromiseEnqueue(value, GET_PRIVATE(promise, promiseQueue), status);
76      PromiseSet(promise, status, value);
77    }
78  }
79
80  function PromiseCoerce(constructor, x) {
81    if (!IsPromise(x) && IS_SPEC_OBJECT(x)) {
82      var then;
83      try {
84        then = x.then;
85      } catch(r) {
86        return %_CallFunction(constructor, r, PromiseRejected);
87      }
88      if (IS_SPEC_FUNCTION(then)) {
89        var deferred = %_CallFunction(constructor, PromiseDeferred);
90        try {
91          %_CallFunction(x, deferred.resolve, deferred.reject, then);
92        } catch(r) {
93          deferred.reject(r);
94        }
95        return deferred.promise;
96      }
97    }
98    return x;
99  }
100
101  function PromiseHandle(value, handler, deferred) {
102    try {
103      %DebugPushPromise(deferred.promise);
104      var result = handler(value);
105      if (result === deferred.promise)
106        throw MakeTypeError('promise_cyclic', [result]);
107      else if (IsPromise(result))
108        %_CallFunction(result, deferred.resolve, deferred.reject, PromiseChain);
109      else
110        deferred.resolve(result);
111    } catch (exception) {
112      try { deferred.reject(exception); } catch (e) { }
113    } finally {
114      %DebugPopPromise();
115    }
116  }
117
118  function PromiseEnqueue(value, tasks, status) {
119    var id, name, instrumenting = DEBUG_IS_ACTIVE;
120    %EnqueueMicrotask(function() {
121      if (instrumenting) {
122        %DebugAsyncTaskEvent({ type: "willHandle", id: id, name: name });
123      }
124      for (var i = 0; i < tasks.length; i += 2) {
125        PromiseHandle(value, tasks[i], tasks[i + 1])
126      }
127      if (instrumenting) {
128        %DebugAsyncTaskEvent({ type: "didHandle", id: id, name: name });
129      }
130    });
131    if (instrumenting) {
132      id = ++lastMicrotaskId;
133      name = status > 0 ? "Promise.resolve" : "Promise.reject";
134      %DebugAsyncTaskEvent({ type: "enqueue", id: id, name: name });
135    }
136  }
137
138  function PromiseIdResolveHandler(x) { return x }
139  function PromiseIdRejectHandler(r) { throw r }
140
141  function PromiseNopResolver() {}
142
143  // -------------------------------------------------------------------
144  // Define exported functions.
145
146  // For bootstrapper.
147
148  IsPromise = function IsPromise(x) {
149    return IS_SPEC_OBJECT(x) && HAS_DEFINED_PRIVATE(x, promiseStatus);
150  }
151
152  PromiseCreate = function PromiseCreate() {
153    return new $Promise(PromiseNopResolver)
154  }
155
156  PromiseResolve = function PromiseResolve(promise, x) {
157    PromiseDone(promise, +1, x, promiseOnResolve)
158  }
159
160  PromiseReject = function PromiseReject(promise, r) {
161    // Check promise status to confirm that this reject has an effect.
162    // Check promiseDebug property to avoid duplicate event.
163    if (DEBUG_IS_ACTIVE &&
164        GET_PRIVATE(promise, promiseStatus) == 0 &&
165        !HAS_DEFINED_PRIVATE(promise, promiseDebug)) {
166      %DebugPromiseRejectEvent(promise, r);
167    }
168    PromiseDone(promise, -1, r, promiseOnReject)
169  }
170
171  // Convenience.
172
173  function PromiseDeferred() {
174    if (this === $Promise) {
175      // Optimized case, avoid extra closure.
176      var promise = PromiseInit(new $Promise(promiseRaw));
177      return {
178        promise: promise,
179        resolve: function(x) { PromiseResolve(promise, x) },
180        reject: function(r) { PromiseReject(promise, r) }
181      };
182    } else {
183      var result = {};
184      result.promise = new this(function(resolve, reject) {
185        result.resolve = resolve;
186        result.reject = reject;
187      })
188      return result;
189    }
190  }
191
192  function PromiseResolved(x) {
193    if (this === $Promise) {
194      // Optimized case, avoid extra closure.
195      return PromiseSet(new $Promise(promiseRaw), +1, x);
196    } else {
197      return new this(function(resolve, reject) { resolve(x) });
198    }
199  }
200
201  function PromiseRejected(r) {
202    if (this === $Promise) {
203      // Optimized case, avoid extra closure.
204      return PromiseSet(new $Promise(promiseRaw), -1, r);
205    } else {
206      return new this(function(resolve, reject) { reject(r) });
207    }
208  }
209
210  // Simple chaining.
211
212  PromiseChain = function PromiseChain(onResolve, onReject) {  // a.k.a.
213                                                               // flatMap
214    onResolve = IS_UNDEFINED(onResolve) ? PromiseIdResolveHandler : onResolve;
215    onReject = IS_UNDEFINED(onReject) ? PromiseIdRejectHandler : onReject;
216    var deferred = %_CallFunction(this.constructor, PromiseDeferred);
217    switch (GET_PRIVATE(this, promiseStatus)) {
218      case UNDEFINED:
219        throw MakeTypeError('not_a_promise', [this]);
220      case 0:  // Pending
221        GET_PRIVATE(this, promiseOnResolve).push(onResolve, deferred);
222        GET_PRIVATE(this, promiseOnReject).push(onReject, deferred);
223        break;
224      case +1:  // Resolved
225        PromiseEnqueue(GET_PRIVATE(this, promiseValue),
226                       [onResolve, deferred],
227                       +1);
228        break;
229      case -1:  // Rejected
230        PromiseEnqueue(GET_PRIVATE(this, promiseValue),
231                       [onReject, deferred],
232                       -1);
233        break;
234    }
235    if (DEBUG_IS_ACTIVE) {
236      %DebugPromiseEvent({ promise: deferred.promise, parentPromise: this });
237    }
238    return deferred.promise;
239  }
240
241  PromiseCatch = function PromiseCatch(onReject) {
242    return this.then(UNDEFINED, onReject);
243  }
244
245  // Multi-unwrapped chaining with thenable coercion.
246
247  PromiseThen = function PromiseThen(onResolve, onReject) {
248    onResolve = IS_SPEC_FUNCTION(onResolve) ? onResolve
249                                            : PromiseIdResolveHandler;
250    onReject = IS_SPEC_FUNCTION(onReject) ? onReject
251                                          : PromiseIdRejectHandler;
252    var that = this;
253    var constructor = this.constructor;
254    return %_CallFunction(
255      this,
256      function(x) {
257        x = PromiseCoerce(constructor, x);
258        return x === that ? onReject(MakeTypeError('promise_cyclic', [x])) :
259               IsPromise(x) ? x.then(onResolve, onReject) : onResolve(x);
260      },
261      onReject,
262      PromiseChain
263    );
264  }
265
266  // Combinators.
267
268  function PromiseCast(x) {
269    // TODO(rossberg): cannot do better until we support @@create.
270    return IsPromise(x) ? x : new this(function(resolve) { resolve(x) });
271  }
272
273  function PromiseAll(values) {
274    var deferred = %_CallFunction(this, PromiseDeferred);
275    var resolutions = [];
276    if (!%_IsArray(values)) {
277      deferred.reject(MakeTypeError('invalid_argument'));
278      return deferred.promise;
279    }
280    try {
281      var count = values.length;
282      if (count === 0) {
283        deferred.resolve(resolutions);
284      } else {
285        for (var i = 0; i < values.length; ++i) {
286          this.resolve(values[i]).then(
287            (function() {
288              // Nested scope to get closure over current i (and avoid .bind).
289              // TODO(rossberg): Use for-let instead once available.
290              var i_captured = i;
291              return function(x) {
292                resolutions[i_captured] = x;
293                if (--count === 0) deferred.resolve(resolutions);
294              };
295            })(),
296            function(r) { deferred.reject(r) }
297          );
298        }
299      }
300    } catch (e) {
301      deferred.reject(e)
302    }
303    return deferred.promise;
304  }
305
306  function PromiseOne(values) {
307    var deferred = %_CallFunction(this, PromiseDeferred);
308    if (!%_IsArray(values)) {
309      deferred.reject(MakeTypeError('invalid_argument'));
310      return deferred.promise;
311    }
312    try {
313      for (var i = 0; i < values.length; ++i) {
314        this.resolve(values[i]).then(
315          function(x) { deferred.resolve(x) },
316          function(r) { deferred.reject(r) }
317        );
318      }
319    } catch (e) {
320      deferred.reject(e)
321    }
322    return deferred.promise;
323  }
324
325
326  // Utility for debugger
327
328  function PromiseHasRejectHandlerRecursive(promise) {
329    var queue = GET_PRIVATE(promise, promiseOnReject);
330    if (IS_UNDEFINED(queue)) return false;
331    // Do a depth first search for a reject handler that's not
332    // the default PromiseIdRejectHandler.
333    for (var i = 0; i < queue.length; i += 2) {
334      if (queue[i] != PromiseIdRejectHandler) return true;
335      if (PromiseHasRejectHandlerRecursive(queue[i + 1].promise)) return true;
336    }
337    return false;
338  }
339
340  PromiseHasRejectHandler = function PromiseHasRejectHandler() {
341    // Mark promise as already having triggered a reject event.
342    SET_PRIVATE(this, promiseDebug, true);
343    return PromiseHasRejectHandlerRecursive(this);
344  };
345
346  // -------------------------------------------------------------------
347  // Install exported functions.
348
349  %CheckIsBootstrapping();
350  %AddNamedProperty(global, 'Promise', $Promise, DONT_ENUM);
351  InstallFunctions($Promise, DONT_ENUM, [
352    "defer", PromiseDeferred,
353    "accept", PromiseResolved,
354    "reject", PromiseRejected,
355    "all", PromiseAll,
356    "race", PromiseOne,
357    "resolve", PromiseCast
358  ]);
359  InstallFunctions($Promise.prototype, DONT_ENUM, [
360    "chain", PromiseChain,
361    "then", PromiseThen,
362    "catch", PromiseCatch
363  ]);
364
365})();
366