• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 promiseRejectReactionsSymbol =
21    utils.ImportNow("promise_reject_reactions_symbol");
22var promiseFulfillReactionsSymbol =
23    utils.ImportNow("promise_fulfill_reactions_symbol");
24var promiseDeferredReactionsSymbol =
25    utils.ImportNow("promise_deferred_reactions_symbol");
26var promiseRawSymbol = utils.ImportNow("promise_raw_symbol");
27var promiseStateSymbol = utils.ImportNow("promise_state_symbol");
28var promiseResultSymbol = utils.ImportNow("promise_result_symbol");
29var SpeciesConstructor;
30var speciesSymbol = utils.ImportNow("species_symbol");
31var toStringTagSymbol = utils.ImportNow("to_string_tag_symbol");
32
33utils.Import(function(from) {
34  MakeTypeError = from.MakeTypeError;
35  SpeciesConstructor = from.SpeciesConstructor;
36});
37
38// -------------------------------------------------------------------
39
40// [[PromiseState]] values:
41const kPending = 0;
42const kFulfilled = +1;
43const kRejected = -1;
44
45var lastMicrotaskId = 0;
46
47// ES#sec-createresolvingfunctions
48// CreateResolvingFunctions ( promise )
49function CreateResolvingFunctions(promise) {
50  var alreadyResolved = false;
51
52  // ES#sec-promise-resolve-functions
53  // Promise Resolve Functions
54  var resolve = value => {
55    if (alreadyResolved === true) return;
56    alreadyResolved = true;
57    ResolvePromise(promise, value);
58  };
59
60  // ES#sec-promise-reject-functions
61  // Promise Reject Functions
62  var reject = reason => {
63    if (alreadyResolved === true) return;
64    alreadyResolved = true;
65    RejectPromise(promise, reason);
66  };
67
68  return {
69    __proto__: null,
70    resolve: resolve,
71    reject: reject
72  };
73}
74
75
76// ES#sec-promise-executor
77// Promise ( executor )
78var GlobalPromise = function Promise(executor) {
79  if (executor === promiseRawSymbol) {
80    return %_NewObject(GlobalPromise, new.target);
81  }
82  if (IS_UNDEFINED(new.target)) throw MakeTypeError(kNotAPromise, this);
83  if (!IS_CALLABLE(executor)) {
84    throw MakeTypeError(kResolverNotAFunction, executor);
85  }
86
87  var promise = PromiseInit(%_NewObject(GlobalPromise, new.target));
88  var callbacks = CreateResolvingFunctions(promise);
89  var debug_is_active = DEBUG_IS_ACTIVE;
90  try {
91    if (debug_is_active) %DebugPushPromise(promise, Promise);
92    executor(callbacks.resolve, callbacks.reject);
93  } catch (e) {
94    %_Call(callbacks.reject, UNDEFINED, e);
95  } finally {
96    if (debug_is_active) %DebugPopPromise();
97  }
98
99  return promise;
100}
101
102// Core functionality.
103
104function PromiseSet(promise, status, value) {
105  SET_PRIVATE(promise, promiseStateSymbol, status);
106  SET_PRIVATE(promise, promiseResultSymbol, value);
107
108  // There are 3 possible states for the resolve, reject symbols when we add
109  // a new callback --
110  // 1) UNDEFINED -- This is the zero state where there is no callback
111  // registered. When we see this state, we directly attach the callbacks to
112  // the symbol.
113  // 2) !IS_ARRAY -- There is a single callback directly attached to the
114  // symbols. We need to create a new array to store additional callbacks.
115  // 3) IS_ARRAY -- There are multiple callbacks already registered,
116  // therefore we can just push the new callback to the existing array.
117  SET_PRIVATE(promise, promiseFulfillReactionsSymbol, UNDEFINED);
118  SET_PRIVATE(promise, promiseRejectReactionsSymbol, UNDEFINED);
119
120  // There are 2 possible states for this symbol --
121  // 1) UNDEFINED -- This is the zero state, no deferred object is
122  // attached to this symbol. When we want to add a new deferred we
123  // directly attach it to this symbol.
124  // 2) symbol with attached deferred object -- New deferred objects
125  // are not attached to this symbol, but instead they are directly
126  // attached to the resolve, reject callback arrays. At this point,
127  // the deferred symbol's state is stale, and the deferreds should be
128  // read from the reject, resolve callbacks.
129  SET_PRIVATE(promise, promiseDeferredReactionsSymbol, UNDEFINED);
130
131  return promise;
132}
133
134function PromiseCreateAndSet(status, value) {
135  var promise = new GlobalPromise(promiseRawSymbol);
136  // If debug is active, notify about the newly created promise first.
137  if (DEBUG_IS_ACTIVE) PromiseSet(promise, kPending, UNDEFINED);
138  return PromiseSet(promise, status, value);
139}
140
141function PromiseInit(promise) {
142  return PromiseSet(promise, kPending, UNDEFINED);
143}
144
145function FulfillPromise(promise, status, value, promiseQueue) {
146  if (GET_PRIVATE(promise, promiseStateSymbol) === kPending) {
147    var tasks = GET_PRIVATE(promise, promiseQueue);
148    if (!IS_UNDEFINED(tasks)) {
149      var tasks = GET_PRIVATE(promise, promiseQueue);
150      var deferreds = GET_PRIVATE(promise, promiseDeferredReactionsSymbol);
151      PromiseEnqueue(value, tasks, deferreds, status);
152    }
153    PromiseSet(promise, status, value);
154  }
155}
156
157function PromiseHandle(value, handler, deferred) {
158  var debug_is_active = DEBUG_IS_ACTIVE;
159  try {
160    if (debug_is_active) %DebugPushPromise(deferred.promise, PromiseHandle);
161    var result = handler(value);
162    deferred.resolve(result);
163  } catch (exception) {
164    try { deferred.reject(exception); } catch (e) { }
165  } finally {
166    if (debug_is_active) %DebugPopPromise();
167  }
168}
169
170function PromiseEnqueue(value, tasks, deferreds, status) {
171  var id, name, instrumenting = DEBUG_IS_ACTIVE;
172  %EnqueueMicrotask(function() {
173    if (instrumenting) {
174      %DebugAsyncTaskEvent({ type: "willHandle", id: id, name: name });
175    }
176    if (IS_ARRAY(tasks)) {
177      for (var i = 0; i < tasks.length; i += 2) {
178        PromiseHandle(value, tasks[i], tasks[i + 1]);
179      }
180    } else {
181      PromiseHandle(value, tasks, deferreds);
182    }
183    if (instrumenting) {
184      %DebugAsyncTaskEvent({ type: "didHandle", id: id, name: name });
185    }
186  });
187  if (instrumenting) {
188    id = ++lastMicrotaskId;
189    name = status === kFulfilled ? "Promise.resolve" : "Promise.reject";
190    %DebugAsyncTaskEvent({ type: "enqueue", id: id, name: name });
191  }
192}
193
194function PromiseAttachCallbacks(promise, deferred, onResolve, onReject) {
195  var maybeResolveCallbacks =
196      GET_PRIVATE(promise, promiseFulfillReactionsSymbol);
197  if (IS_UNDEFINED(maybeResolveCallbacks)) {
198    SET_PRIVATE(promise, promiseFulfillReactionsSymbol, onResolve);
199    SET_PRIVATE(promise, promiseRejectReactionsSymbol, onReject);
200    SET_PRIVATE(promise, promiseDeferredReactionsSymbol, deferred);
201  } else if (!IS_ARRAY(maybeResolveCallbacks)) {
202    var resolveCallbacks = new InternalArray();
203    var rejectCallbacks = new InternalArray();
204    var existingDeferred = GET_PRIVATE(promise, promiseDeferredReactionsSymbol);
205
206    resolveCallbacks.push(
207        maybeResolveCallbacks, existingDeferred, onResolve, deferred);
208    rejectCallbacks.push(GET_PRIVATE(promise, promiseRejectReactionsSymbol),
209                         existingDeferred,
210                         onReject,
211                         deferred);
212
213    SET_PRIVATE(promise, promiseFulfillReactionsSymbol, resolveCallbacks);
214    SET_PRIVATE(promise, promiseRejectReactionsSymbol, rejectCallbacks);
215  } else {
216    maybeResolveCallbacks.push(onResolve, deferred);
217    GET_PRIVATE(promise, promiseRejectReactionsSymbol).push(onReject, deferred);
218  }
219}
220
221function PromiseIdResolveHandler(x) { return x }
222function PromiseIdRejectHandler(r) { throw r }
223
224function PromiseNopResolver() {}
225
226// -------------------------------------------------------------------
227// Define exported functions.
228
229// For bootstrapper.
230
231// ES#sec-ispromise IsPromise ( x )
232function IsPromise(x) {
233  return IS_RECEIVER(x) && HAS_DEFINED_PRIVATE(x, promiseStateSymbol);
234}
235
236function PromiseCreate() {
237  return new GlobalPromise(PromiseNopResolver)
238}
239
240// ES#sec-promise-resolve-functions
241// Promise Resolve Functions, steps 6-13
242function ResolvePromise(promise, resolution) {
243  if (resolution === promise) {
244    return RejectPromise(promise, MakeTypeError(kPromiseCyclic, resolution));
245  }
246  if (IS_RECEIVER(resolution)) {
247    // 25.4.1.3.2 steps 8-12
248    try {
249      var then = resolution.then;
250    } catch (e) {
251      return RejectPromise(promise, e);
252    }
253
254    // Resolution is a native promise and if it's already resolved or
255    // rejected, shortcircuit the resolution procedure by directly
256    // reusing the value from the promise.
257    if (IsPromise(resolution) && then === PromiseThen) {
258      var thenableState = GET_PRIVATE(resolution, promiseStateSymbol);
259      if (thenableState === kFulfilled) {
260        // This goes inside the if-else to save one symbol lookup in
261        // the slow path.
262        var thenableValue = GET_PRIVATE(resolution, promiseResultSymbol);
263        FulfillPromise(promise, kFulfilled, thenableValue,
264                       promiseFulfillReactionsSymbol);
265        SET_PRIVATE(promise, promiseHasHandlerSymbol, true);
266        return;
267      } else if (thenableState === kRejected) {
268        var thenableValue = GET_PRIVATE(resolution, promiseResultSymbol);
269        if (!HAS_DEFINED_PRIVATE(resolution, promiseHasHandlerSymbol)) {
270          // Promise has already been rejected, but had no handler.
271          // Revoke previously triggered reject event.
272          %PromiseRevokeReject(resolution);
273        }
274        RejectPromise(promise, thenableValue);
275        SET_PRIVATE(resolution, promiseHasHandlerSymbol, true);
276        return;
277      }
278    }
279
280    if (IS_CALLABLE(then)) {
281      // PromiseResolveThenableJob
282      var id;
283      var name = "PromiseResolveThenableJob";
284      var instrumenting = DEBUG_IS_ACTIVE;
285      %EnqueueMicrotask(function() {
286        if (instrumenting) {
287          %DebugAsyncTaskEvent({ type: "willHandle", id: id, name: name });
288        }
289        var callbacks = CreateResolvingFunctions(promise);
290        try {
291          %_Call(then, resolution, callbacks.resolve, callbacks.reject);
292        } catch (e) {
293          %_Call(callbacks.reject, UNDEFINED, e);
294        }
295        if (instrumenting) {
296          %DebugAsyncTaskEvent({ type: "didHandle", id: id, name: name });
297        }
298      });
299      if (instrumenting) {
300        id = ++lastMicrotaskId;
301        %DebugAsyncTaskEvent({ type: "enqueue", id: id, name: name });
302      }
303      return;
304    }
305  }
306  FulfillPromise(promise, kFulfilled, resolution, promiseFulfillReactionsSymbol);
307}
308
309// ES#sec-rejectpromise
310// RejectPromise ( promise, reason )
311function RejectPromise(promise, reason) {
312  // Check promise status to confirm that this reject has an effect.
313  // Call runtime for callbacks to the debugger or for unhandled reject.
314  if (GET_PRIVATE(promise, promiseStateSymbol) === kPending) {
315    var debug_is_active = DEBUG_IS_ACTIVE;
316    if (debug_is_active ||
317        !HAS_DEFINED_PRIVATE(promise, promiseHasHandlerSymbol)) {
318      %PromiseRejectEvent(promise, reason, debug_is_active);
319    }
320  }
321  FulfillPromise(promise, kRejected, reason, promiseRejectReactionsSymbol)
322}
323
324// ES#sec-newpromisecapability
325// NewPromiseCapability ( C )
326function NewPromiseCapability(C) {
327  if (C === GlobalPromise) {
328    // Optimized case, avoid extra closure.
329    var promise = PromiseInit(new GlobalPromise(promiseRawSymbol));
330    var callbacks = CreateResolvingFunctions(promise);
331    return {
332      promise: promise,
333      resolve: callbacks.resolve,
334      reject: callbacks.reject
335    };
336  }
337
338  var result = {promise: UNDEFINED, resolve: UNDEFINED, reject: UNDEFINED };
339  result.promise = new C((resolve, reject) => {
340    if (!IS_UNDEFINED(result.resolve) || !IS_UNDEFINED(result.reject))
341        throw MakeTypeError(kPromiseExecutorAlreadyInvoked);
342    result.resolve = resolve;
343    result.reject = reject;
344  });
345
346  if (!IS_CALLABLE(result.resolve) || !IS_CALLABLE(result.reject))
347      throw MakeTypeError(kPromiseNonCallable);
348
349  return result;
350}
351
352// Unspecified V8-specific legacy function
353function PromiseDefer() {
354  %IncrementUseCounter(kPromiseDefer);
355  return NewPromiseCapability(this);
356}
357
358// Unspecified V8-specific legacy function
359function PromiseAccept(x) {
360  %IncrementUseCounter(kPromiseAccept);
361  return %_Call(PromiseResolve, this, x);
362}
363
364// ES#sec-promise.reject
365// Promise.reject ( x )
366function PromiseReject(r) {
367  if (!IS_RECEIVER(this)) {
368    throw MakeTypeError(kCalledOnNonObject, PromiseResolve);
369  }
370  if (this === GlobalPromise) {
371    // Optimized case, avoid extra closure.
372    var promise = PromiseCreateAndSet(kRejected, r);
373    // The debug event for this would always be an uncaught promise reject,
374    // which is usually simply noise. Do not trigger that debug event.
375    %PromiseRejectEvent(promise, r, false);
376    return promise;
377  } else {
378    var promiseCapability = NewPromiseCapability(this);
379    %_Call(promiseCapability.reject, UNDEFINED, r);
380    return promiseCapability.promise;
381  }
382}
383
384// Shortcut Promise.reject and Promise.resolve() implementations, used by
385// Async Functions implementation.
386function PromiseCreateRejected(r) {
387  return %_Call(PromiseReject, GlobalPromise, r);
388}
389
390function PromiseCreateResolved(x) {
391  return %_Call(PromiseResolve, GlobalPromise, x);
392}
393
394// ES#sec-promise.prototype.then
395// Promise.prototype.then ( onFulfilled, onRejected )
396// Multi-unwrapped chaining with thenable coercion.
397function PromiseThen(onResolve, onReject) {
398  var status = GET_PRIVATE(this, promiseStateSymbol);
399  if (IS_UNDEFINED(status)) {
400    throw MakeTypeError(kNotAPromise, this);
401  }
402
403  var constructor = SpeciesConstructor(this, GlobalPromise);
404  onResolve = IS_CALLABLE(onResolve) ? onResolve : PromiseIdResolveHandler;
405  onReject = IS_CALLABLE(onReject) ? onReject : PromiseIdRejectHandler;
406  var deferred = NewPromiseCapability(constructor);
407  switch (status) {
408    case kPending:
409      PromiseAttachCallbacks(this, deferred, onResolve, onReject);
410      break;
411    case kFulfilled:
412      PromiseEnqueue(GET_PRIVATE(this, promiseResultSymbol),
413                     onResolve, deferred, kFulfilled);
414      break;
415    case kRejected:
416      if (!HAS_DEFINED_PRIVATE(this, promiseHasHandlerSymbol)) {
417        // Promise has already been rejected, but had no handler.
418        // Revoke previously triggered reject event.
419        %PromiseRevokeReject(this);
420      }
421      PromiseEnqueue(GET_PRIVATE(this, promiseResultSymbol),
422                     onReject, deferred, kRejected);
423      break;
424  }
425  // Mark this promise as having handler.
426  SET_PRIVATE(this, promiseHasHandlerSymbol, true);
427  return deferred.promise;
428}
429
430// Unspecified V8-specific legacy function
431// Chain is left around for now as an alias for then
432function PromiseChain(onResolve, onReject) {
433  %IncrementUseCounter(kPromiseChain);
434  return %_Call(PromiseThen, this, onResolve, onReject);
435}
436
437// ES#sec-promise.prototype.catch
438// Promise.prototype.catch ( onRejected )
439function PromiseCatch(onReject) {
440  return this.then(UNDEFINED, onReject);
441}
442
443// Combinators.
444
445// ES#sec-promise.resolve
446// Promise.resolve ( x )
447function PromiseResolve(x) {
448  if (!IS_RECEIVER(this)) {
449    throw MakeTypeError(kCalledOnNonObject, PromiseResolve);
450  }
451  if (IsPromise(x) && x.constructor === this) return x;
452
453  var promiseCapability = NewPromiseCapability(this);
454  var resolveResult = %_Call(promiseCapability.resolve, UNDEFINED, x);
455  return promiseCapability.promise;
456}
457
458// ES#sec-promise.all
459// Promise.all ( iterable )
460function PromiseAll(iterable) {
461  if (!IS_RECEIVER(this)) {
462    throw MakeTypeError(kCalledOnNonObject, "Promise.all");
463  }
464
465  var deferred = NewPromiseCapability(this);
466  var resolutions = new InternalArray();
467  var count;
468
469  function CreateResolveElementFunction(index, values, promiseCapability) {
470    var alreadyCalled = false;
471    return (x) => {
472      if (alreadyCalled === true) return;
473      alreadyCalled = true;
474      values[index] = x;
475      if (--count === 0) {
476        var valuesArray = [];
477        %MoveArrayContents(values, valuesArray);
478        %_Call(promiseCapability.resolve, UNDEFINED, valuesArray);
479      }
480    };
481  }
482
483  try {
484    var i = 0;
485    count = 1;
486    for (var value of iterable) {
487      var nextPromise = this.resolve(value);
488      ++count;
489      nextPromise.then(
490          CreateResolveElementFunction(i, resolutions, deferred),
491          deferred.reject);
492      SET_PRIVATE(deferred.reject, promiseCombinedDeferredSymbol, deferred);
493      ++i;
494    }
495
496    // 6.d
497    if (--count === 0) {
498      var valuesArray = [];
499      %MoveArrayContents(resolutions, valuesArray);
500      %_Call(deferred.resolve, UNDEFINED, valuesArray);
501    }
502
503  } catch (e) {
504    %_Call(deferred.reject, UNDEFINED, e);
505  }
506  return deferred.promise;
507}
508
509// ES#sec-promise.race
510// Promise.race ( iterable )
511function PromiseRace(iterable) {
512  if (!IS_RECEIVER(this)) {
513    throw MakeTypeError(kCalledOnNonObject, PromiseRace);
514  }
515
516  var deferred = NewPromiseCapability(this);
517  try {
518    for (var value of iterable) {
519      this.resolve(value).then(deferred.resolve, deferred.reject);
520      SET_PRIVATE(deferred.reject, promiseCombinedDeferredSymbol, deferred);
521    }
522  } catch (e) {
523    deferred.reject(e)
524  }
525  return deferred.promise;
526}
527
528
529// Utility for debugger
530
531function PromiseHasUserDefinedRejectHandlerCheck(handler, deferred) {
532  if (handler !== PromiseIdRejectHandler) {
533    var combinedDeferred = GET_PRIVATE(handler, promiseCombinedDeferredSymbol);
534    if (IS_UNDEFINED(combinedDeferred)) return true;
535    if (PromiseHasUserDefinedRejectHandlerRecursive(combinedDeferred.promise)) {
536      return true;
537    }
538  } else if (PromiseHasUserDefinedRejectHandlerRecursive(deferred.promise)) {
539    return true;
540  }
541  return false;
542}
543
544function PromiseHasUserDefinedRejectHandlerRecursive(promise) {
545  var queue = GET_PRIVATE(promise, promiseRejectReactionsSymbol);
546  var deferreds = GET_PRIVATE(promise, promiseDeferredReactionsSymbol);
547  if (IS_UNDEFINED(queue)) return false;
548  if (!IS_ARRAY(queue)) {
549    return PromiseHasUserDefinedRejectHandlerCheck(queue, deferreds);
550  } else {
551    for (var i = 0; i < queue.length; i += 2) {
552      if (PromiseHasUserDefinedRejectHandlerCheck(queue[i], queue[i + 1])) {
553        return true;
554      }
555    }
556  }
557  return false;
558}
559
560// Return whether the promise will be handled by a user-defined reject
561// handler somewhere down the promise chain. For this, we do a depth-first
562// search for a reject handler that's not the default PromiseIdRejectHandler.
563function PromiseHasUserDefinedRejectHandler() {
564  return PromiseHasUserDefinedRejectHandlerRecursive(this);
565};
566
567
568function PromiseSpecies() {
569  return this;
570}
571
572// -------------------------------------------------------------------
573// Install exported functions.
574
575%AddNamedProperty(global, 'Promise', GlobalPromise, DONT_ENUM);
576%AddNamedProperty(GlobalPromise.prototype, toStringTagSymbol, "Promise",
577                  DONT_ENUM | READ_ONLY);
578
579utils.InstallFunctions(GlobalPromise, DONT_ENUM, [
580  "reject", PromiseReject,
581  "all", PromiseAll,
582  "race", PromiseRace,
583  "resolve", PromiseResolve
584]);
585
586utils.InstallGetter(GlobalPromise, speciesSymbol, PromiseSpecies);
587
588utils.InstallFunctions(GlobalPromise.prototype, DONT_ENUM, [
589  "then", PromiseThen,
590  "catch", PromiseCatch
591]);
592
593%InstallToContext([
594  "promise_catch", PromiseCatch,
595  "promise_chain", PromiseChain,
596  "promise_create", PromiseCreate,
597  "promise_has_user_defined_reject_handler", PromiseHasUserDefinedRejectHandler,
598  "promise_reject", RejectPromise,
599  "promise_resolve", ResolvePromise,
600  "promise_then", PromiseThen,
601  "promise_create_rejected", PromiseCreateRejected,
602  "promise_create_resolved", PromiseCreateResolved
603]);
604
605// This allows extras to create promises quickly without building extra
606// resolve/reject closures, and allows them to later resolve and reject any
607// promise without having to hold on to those closures forever.
608utils.InstallFunctions(extrasUtils, 0, [
609  "createPromise", PromiseCreate,
610  "resolvePromise", ResolvePromise,
611  "rejectPromise", RejectPromise
612]);
613
614// TODO(v8:4567): Allow experimental natives to remove function prototype
615[PromiseChain, PromiseDefer, PromiseAccept].forEach(
616    fn => %FunctionRemovePrototype(fn));
617
618utils.Export(function(to) {
619  to.PromiseChain = PromiseChain;
620  to.PromiseDefer = PromiseDefer;
621  to.PromiseAccept = PromiseAccept;
622
623  to.PromiseCreateRejected = PromiseCreateRejected;
624  to.PromiseCreateResolved = PromiseCreateResolved;
625  to.PromiseThen = PromiseThen;
626});
627
628})
629