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(function(global, utils, extrasUtils) {
6
7"use strict";
8
9%CheckIsBootstrapping();
10
11// -------------------------------------------------------------------
12// Imports
13
14var InternalArray = utils.InternalArray;
15var MakeTypeError;
16var promiseCombinedDeferredSymbol =
17    utils.ImportNow("promise_combined_deferred_symbol");
18var promiseHasHandlerSymbol =
19    utils.ImportNow("promise_has_handler_symbol");
20var promiseOnRejectSymbol = utils.ImportNow("promise_on_reject_symbol");
21var promiseOnResolveSymbol =
22    utils.ImportNow("promise_on_resolve_symbol");
23var promiseRawSymbol = utils.ImportNow("promise_raw_symbol");
24var promiseStatusSymbol = utils.ImportNow("promise_status_symbol");
25var promiseValueSymbol = utils.ImportNow("promise_value_symbol");
26var SpeciesConstructor;
27var toStringTagSymbol = utils.ImportNow("to_string_tag_symbol");
28
29utils.Import(function(from) {
30  MakeTypeError = from.MakeTypeError;
31  SpeciesConstructor = from.SpeciesConstructor;
32});
33
34// -------------------------------------------------------------------
35
36// Status values: 0 = pending, +1 = resolved, -1 = rejected
37var lastMicrotaskId = 0;
38
39function CreateResolvingFunctions(promise) {
40  var alreadyResolved = false;
41
42  var resolve = value => {
43    if (alreadyResolved === true) return;
44    alreadyResolved = true;
45    PromiseResolve(promise, value);
46  };
47
48  var reject = reason => {
49    if (alreadyResolved === true) return;
50    alreadyResolved = true;
51    PromiseReject(promise, reason);
52  };
53
54  return {
55    __proto__: null,
56    resolve: resolve,
57    reject: reject
58  };
59}
60
61
62var GlobalPromise = function Promise(resolver) {
63  if (resolver === promiseRawSymbol) {
64    return %NewObject(GlobalPromise, new.target);
65  }
66  if (IS_UNDEFINED(new.target)) throw MakeTypeError(kNotAPromise, this);
67  if (!IS_CALLABLE(resolver))
68    throw MakeTypeError(kResolverNotAFunction, resolver);
69
70  var promise = PromiseInit(%NewObject(GlobalPromise, new.target));
71  var callbacks = CreateResolvingFunctions(promise);
72
73  try {
74    %DebugPushPromise(promise, Promise);
75    resolver(callbacks.resolve, callbacks.reject);
76  } catch (e) {
77    %_Call(callbacks.reject, UNDEFINED, e);
78  } finally {
79    %DebugPopPromise();
80  }
81
82  return promise;
83}
84
85// Core functionality.
86
87function PromiseSet(promise, status, value, onResolve, onReject) {
88  SET_PRIVATE(promise, promiseStatusSymbol, status);
89  SET_PRIVATE(promise, promiseValueSymbol, value);
90  SET_PRIVATE(promise, promiseOnResolveSymbol, onResolve);
91  SET_PRIVATE(promise, promiseOnRejectSymbol, onReject);
92  if (DEBUG_IS_ACTIVE) {
93    %DebugPromiseEvent({ promise: promise, status: status, value: value });
94  }
95  return promise;
96}
97
98function PromiseCreateAndSet(status, value) {
99  var promise = new GlobalPromise(promiseRawSymbol);
100  // If debug is active, notify about the newly created promise first.
101  if (DEBUG_IS_ACTIVE) PromiseSet(promise, 0, UNDEFINED);
102  return PromiseSet(promise, status, value);
103}
104
105function PromiseInit(promise) {
106  return PromiseSet(
107      promise, 0, UNDEFINED, new InternalArray, new InternalArray)
108}
109
110function PromiseDone(promise, status, value, promiseQueue) {
111  if (GET_PRIVATE(promise, promiseStatusSymbol) === 0) {
112    var tasks = GET_PRIVATE(promise, promiseQueue);
113    if (tasks.length) PromiseEnqueue(value, tasks, status);
114    PromiseSet(promise, status, value);
115  }
116}
117
118function PromiseHandle(value, handler, deferred) {
119  try {
120    %DebugPushPromise(deferred.promise, PromiseHandle);
121    var result = handler(value);
122    deferred.resolve(result);
123  } catch (exception) {
124    try { deferred.reject(exception); } catch (e) { }
125  } finally {
126    %DebugPopPromise();
127  }
128}
129
130function PromiseEnqueue(value, tasks, status) {
131  var id, name, instrumenting = DEBUG_IS_ACTIVE;
132  %EnqueueMicrotask(function() {
133    if (instrumenting) {
134      %DebugAsyncTaskEvent({ type: "willHandle", id: id, name: name });
135    }
136    for (var i = 0; i < tasks.length; i += 2) {
137      PromiseHandle(value, tasks[i], tasks[i + 1])
138    }
139    if (instrumenting) {
140      %DebugAsyncTaskEvent({ type: "didHandle", id: id, name: name });
141    }
142  });
143  if (instrumenting) {
144    id = ++lastMicrotaskId;
145    name = status > 0 ? "Promise.resolve" : "Promise.reject";
146    %DebugAsyncTaskEvent({ type: "enqueue", id: id, name: name });
147  }
148}
149
150function PromiseIdResolveHandler(x) { return x }
151function PromiseIdRejectHandler(r) { throw r }
152
153function PromiseNopResolver() {}
154
155// -------------------------------------------------------------------
156// Define exported functions.
157
158// For bootstrapper.
159
160function IsPromise(x) {
161  return IS_RECEIVER(x) && HAS_DEFINED_PRIVATE(x, promiseStatusSymbol);
162}
163
164function PromiseCreate() {
165  return new GlobalPromise(PromiseNopResolver)
166}
167
168function PromiseResolve(promise, x) {
169  if (x === promise) {
170    return PromiseReject(promise, MakeTypeError(kPromiseCyclic, x));
171  }
172  if (IS_RECEIVER(x)) {
173    // 25.4.1.3.2 steps 8-12
174    try {
175      var then = x.then;
176    } catch (e) {
177      return PromiseReject(promise, e);
178    }
179    if (IS_CALLABLE(then)) {
180      // PromiseResolveThenableJob
181      var id, name, instrumenting = DEBUG_IS_ACTIVE;
182      %EnqueueMicrotask(function() {
183        if (instrumenting) {
184          %DebugAsyncTaskEvent({ type: "willHandle", id: id, name: name });
185        }
186        var callbacks = CreateResolvingFunctions(promise);
187        try {
188          %_Call(then, x, callbacks.resolve, callbacks.reject);
189        } catch (e) {
190          %_Call(callbacks.reject, UNDEFINED, e);
191        }
192        if (instrumenting) {
193          %DebugAsyncTaskEvent({ type: "didHandle", id: id, name: name });
194        }
195      });
196      if (instrumenting) {
197        id = ++lastMicrotaskId;
198        name = "PromseResolveThenableJob";
199        %DebugAsyncTaskEvent({ type: "enqueue", id: id, name: name });
200      }
201      return;
202    }
203  }
204  PromiseDone(promise, +1, x, promiseOnResolveSymbol);
205}
206
207function PromiseReject(promise, r) {
208  // Check promise status to confirm that this reject has an effect.
209  // Call runtime for callbacks to the debugger or for unhandled reject.
210  if (GET_PRIVATE(promise, promiseStatusSymbol) == 0) {
211    var debug_is_active = DEBUG_IS_ACTIVE;
212    if (debug_is_active ||
213        !HAS_DEFINED_PRIVATE(promise, promiseHasHandlerSymbol)) {
214      %PromiseRejectEvent(promise, r, debug_is_active);
215    }
216  }
217  PromiseDone(promise, -1, r, promiseOnRejectSymbol)
218}
219
220// Convenience.
221
222function NewPromiseCapability(C) {
223  if (C === GlobalPromise) {
224    // Optimized case, avoid extra closure.
225    var promise = PromiseInit(new GlobalPromise(promiseRawSymbol));
226    var callbacks = CreateResolvingFunctions(promise);
227    return {
228      promise: promise,
229      resolve: callbacks.resolve,
230      reject: callbacks.reject
231    };
232  }
233
234  var result = {promise: UNDEFINED, resolve: UNDEFINED, reject: UNDEFINED };
235  result.promise = new C((resolve, reject) => {
236    if (!IS_UNDEFINED(result.resolve) || !IS_UNDEFINED(result.reject))
237        throw MakeTypeError(kPromiseExecutorAlreadyInvoked);
238    result.resolve = resolve;
239    result.reject = reject;
240  });
241
242  return result;
243}
244
245function PromiseDeferred() {
246  %IncrementUseCounter(kPromiseDefer);
247  return NewPromiseCapability(this);
248}
249
250function PromiseResolved(x) {
251  %IncrementUseCounter(kPromiseAccept);
252  return %_Call(PromiseCast, this, x);
253}
254
255function PromiseRejected(r) {
256  if (!IS_RECEIVER(this)) {
257    throw MakeTypeError(kCalledOnNonObject, PromiseRejected);
258  }
259  if (this === GlobalPromise) {
260    // Optimized case, avoid extra closure.
261    var promise = PromiseCreateAndSet(-1, r);
262    // The debug event for this would always be an uncaught promise reject,
263    // which is usually simply noise. Do not trigger that debug event.
264    %PromiseRejectEvent(promise, r, false);
265    return promise;
266  } else {
267    var promiseCapability = NewPromiseCapability(this);
268    %_Call(promiseCapability.reject, UNDEFINED, r);
269    return promiseCapability.promise;
270  }
271}
272
273// Multi-unwrapped chaining with thenable coercion.
274
275function PromiseThen(onResolve, onReject) {
276  var status = GET_PRIVATE(this, promiseStatusSymbol);
277  if (IS_UNDEFINED(status)) {
278    throw MakeTypeError(kNotAPromise, this);
279  }
280
281  var constructor = SpeciesConstructor(this, GlobalPromise);
282  onResolve = IS_CALLABLE(onResolve) ? onResolve : PromiseIdResolveHandler;
283  onReject = IS_CALLABLE(onReject) ? onReject : PromiseIdRejectHandler;
284  var deferred = NewPromiseCapability(constructor);
285  switch (status) {
286    case 0:  // Pending
287      GET_PRIVATE(this, promiseOnResolveSymbol).push(onResolve, deferred);
288      GET_PRIVATE(this, promiseOnRejectSymbol).push(onReject, deferred);
289      break;
290    case +1:  // Resolved
291      PromiseEnqueue(GET_PRIVATE(this, promiseValueSymbol),
292                     [onResolve, deferred],
293                     +1);
294      break;
295    case -1:  // Rejected
296      if (!HAS_DEFINED_PRIVATE(this, promiseHasHandlerSymbol)) {
297        // Promise has already been rejected, but had no handler.
298        // Revoke previously triggered reject event.
299        %PromiseRevokeReject(this);
300      }
301      PromiseEnqueue(GET_PRIVATE(this, promiseValueSymbol),
302                     [onReject, deferred],
303                     -1);
304      break;
305  }
306  // Mark this promise as having handler.
307  SET_PRIVATE(this, promiseHasHandlerSymbol, true);
308  if (DEBUG_IS_ACTIVE) {
309    %DebugPromiseEvent({ promise: deferred.promise, parentPromise: this });
310  }
311  return deferred.promise;
312}
313
314// Chain is left around for now as an alias for then
315function PromiseChain(onResolve, onReject) {
316  %IncrementUseCounter(kPromiseChain);
317  return %_Call(PromiseThen, this, onResolve, onReject);
318}
319
320function PromiseCatch(onReject) {
321  return this.then(UNDEFINED, onReject);
322}
323
324// Combinators.
325
326function PromiseCast(x) {
327  if (!IS_RECEIVER(this)) {
328    throw MakeTypeError(kCalledOnNonObject, PromiseCast);
329  }
330  if (IsPromise(x) && x.constructor === this) return x;
331
332  var promiseCapability = NewPromiseCapability(this);
333  var resolveResult = %_Call(promiseCapability.resolve, UNDEFINED, x);
334  return promiseCapability.promise;
335}
336
337function PromiseAll(iterable) {
338  if (!IS_RECEIVER(this)) {
339    throw MakeTypeError(kCalledOnNonObject, "Promise.all");
340  }
341
342  var deferred = NewPromiseCapability(this);
343  var resolutions = new InternalArray();
344  var count;
345
346  function CreateResolveElementFunction(index, values, promiseCapability) {
347    var alreadyCalled = false;
348    return (x) => {
349      if (alreadyCalled === true) return;
350      alreadyCalled = true;
351      values[index] = x;
352      if (--count === 0) {
353        var valuesArray = [];
354        %MoveArrayContents(values, valuesArray);
355        %_Call(promiseCapability.resolve, UNDEFINED, valuesArray);
356      }
357    };
358  }
359
360  try {
361    var i = 0;
362    count = 1;
363    for (var value of iterable) {
364      var nextPromise = this.resolve(value);
365      ++count;
366      nextPromise.then(
367          CreateResolveElementFunction(i, resolutions, deferred),
368          deferred.reject);
369      SET_PRIVATE(deferred.reject, promiseCombinedDeferredSymbol, deferred);
370      ++i;
371    }
372
373    // 6.d
374    if (--count === 0) {
375      var valuesArray = [];
376      %MoveArrayContents(resolutions, valuesArray);
377      %_Call(deferred.resolve, UNDEFINED, valuesArray);
378    }
379
380  } catch (e) {
381    %_Call(deferred.reject, UNDEFINED, e);
382  }
383  return deferred.promise;
384}
385
386function PromiseRace(iterable) {
387  if (!IS_RECEIVER(this)) {
388    throw MakeTypeError(kCalledOnNonObject, PromiseRace);
389  }
390
391  var deferred = NewPromiseCapability(this);
392  try {
393    for (var value of iterable) {
394      this.resolve(value).then(deferred.resolve, deferred.reject);
395      SET_PRIVATE(deferred.reject, promiseCombinedDeferredSymbol, deferred);
396    }
397  } catch (e) {
398    deferred.reject(e)
399  }
400  return deferred.promise;
401}
402
403
404// Utility for debugger
405
406function PromiseHasUserDefinedRejectHandlerRecursive(promise) {
407  var queue = GET_PRIVATE(promise, promiseOnRejectSymbol);
408  if (IS_UNDEFINED(queue)) return false;
409  for (var i = 0; i < queue.length; i += 2) {
410    var handler = queue[i];
411    if (handler !== PromiseIdRejectHandler) {
412      var deferred = GET_PRIVATE(handler, promiseCombinedDeferredSymbol);
413      if (IS_UNDEFINED(deferred)) return true;
414      if (PromiseHasUserDefinedRejectHandlerRecursive(deferred.promise)) {
415        return true;
416      }
417    } else if (PromiseHasUserDefinedRejectHandlerRecursive(
418                   queue[i + 1].promise)) {
419      return true;
420    }
421  }
422  return false;
423}
424
425// Return whether the promise will be handled by a user-defined reject
426// handler somewhere down the promise chain. For this, we do a depth-first
427// search for a reject handler that's not the default PromiseIdRejectHandler.
428function PromiseHasUserDefinedRejectHandler() {
429  return PromiseHasUserDefinedRejectHandlerRecursive(this);
430};
431
432// -------------------------------------------------------------------
433// Install exported functions.
434
435%AddNamedProperty(global, 'Promise', GlobalPromise, DONT_ENUM);
436%AddNamedProperty(GlobalPromise.prototype, toStringTagSymbol, "Promise",
437                  DONT_ENUM | READ_ONLY);
438
439utils.InstallFunctions(GlobalPromise, DONT_ENUM, [
440  "reject", PromiseRejected,
441  "all", PromiseAll,
442  "race", PromiseRace,
443  "resolve", PromiseCast
444]);
445
446utils.InstallFunctions(GlobalPromise.prototype, DONT_ENUM, [
447  "then", PromiseThen,
448  "catch", PromiseCatch
449]);
450
451%InstallToContext([
452  "promise_catch", PromiseCatch,
453  "promise_chain", PromiseChain,
454  "promise_create", PromiseCreate,
455  "promise_has_user_defined_reject_handler", PromiseHasUserDefinedRejectHandler,
456  "promise_reject", PromiseReject,
457  "promise_resolve", PromiseResolve,
458  "promise_then", PromiseThen,
459]);
460
461// This allows extras to create promises quickly without building extra
462// resolve/reject closures, and allows them to later resolve and reject any
463// promise without having to hold on to those closures forever.
464utils.InstallFunctions(extrasUtils, 0, [
465  "createPromise", PromiseCreate,
466  "resolvePromise", PromiseResolve,
467  "rejectPromise", PromiseReject
468]);
469
470// TODO(v8:4567): Allow experimental natives to remove function prototype
471[PromiseChain, PromiseDeferred, PromiseResolved].forEach(
472    fn => %FunctionRemovePrototype(fn));
473
474utils.Export(function(to) {
475  to.PromiseChain = PromiseChain;
476  to.PromiseDeferred = PromiseDeferred;
477  to.PromiseResolved = PromiseResolved;
478});
479
480})
481