promise.js 12.5 KB
Newer Older
rossberg@chromium.org's avatar
rossberg@chromium.org committed
1
// Copyright 2012 the V8 project authors. All rights reserved.
2 3
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
rossberg@chromium.org's avatar
rossberg@chromium.org committed
4 5 6 7 8 9 10 11

"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

12
// For bootstrapper.
rossberg@chromium.org's avatar
rossberg@chromium.org committed
13

14 15 16 17 18 19
var IsPromise;
var PromiseCreate;
var PromiseResolve;
var PromiseReject;
var PromiseChain;
var PromiseCatch;
20
var PromiseThen;
21
var PromiseHasRejectHandler;
22
var PromiseHasUserDefinedRejectHandler;
rossberg@chromium.org's avatar
rossberg@chromium.org committed
23

24 25 26
// mirror-debugger.js currently uses builtins.promiseStatus. It would be nice
// if we could move these property names into the closure below.
// TODO(jkummerow/rossberg/yangguo): Find a better solution.
rossberg@chromium.org's avatar
rossberg@chromium.org committed
27 28

// Status values: 0 = pending, +1 = resolved, -1 = rejected
29 30 31 32 33
var promiseStatus = GLOBAL_PRIVATE("Promise#status");
var promiseValue = GLOBAL_PRIVATE("Promise#value");
var promiseOnResolve = GLOBAL_PRIVATE("Promise#onResolve");
var promiseOnReject = GLOBAL_PRIVATE("Promise#onReject");
var promiseRaw = GLOBAL_PRIVATE("Promise#raw");
34
var promiseHasHandler = %PromiseHasHandlerSymbol();
35
var lastMicrotaskId = 0;
rossberg@chromium.org's avatar
rossberg@chromium.org committed
36

37

38 39 40 41 42 43 44 45 46
(function() {

  var $Promise = function Promise(resolver) {
    if (resolver === promiseRaw) return;
    if (!%_IsConstructCall()) throw MakeTypeError('not_a_promise', [this]);
    if (!IS_SPEC_FUNCTION(resolver))
      throw MakeTypeError('resolver_not_a_function', [resolver]);
    var promise = PromiseInit(this);
    try {
47
      %DebugPushPromise(promise);
48 49 50 51 52
      resolver(function(x) { PromiseResolve(promise, x) },
               function(r) { PromiseReject(promise, r) });
    } catch (e) {
      PromiseReject(promise, e);
    } finally {
53
      %DebugPopPromise();
54
    }
55
  }
rossberg@chromium.org's avatar
rossberg@chromium.org committed
56

57
  // Core functionality.
rossberg@chromium.org's avatar
rossberg@chromium.org committed
58

59 60 61 62 63
  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);
64 65
    if (DEBUG_IS_ACTIVE) {
      %DebugPromiseEvent({ promise: promise, status: status, value: value });
66
    }
67 68
    return promise;
  }
rossberg@chromium.org's avatar
rossberg@chromium.org committed
69

70 71 72 73 74 75 76
  function PromiseCreateAndSet(status, value) {
    var promise = new $Promise(promiseRaw);
    // If debug is active, notify about the newly created promise first.
    if (DEBUG_IS_ACTIVE) PromiseSet(promise, 0, UNDEFINED);
    return PromiseSet(promise, status, value);
  }

77
  function PromiseInit(promise) {
78 79
    return PromiseSet(
        promise, 0, UNDEFINED, new InternalArray, new InternalArray)
80
  }
rossberg@chromium.org's avatar
rossberg@chromium.org committed
81

82 83
  function PromiseDone(promise, status, value, promiseQueue) {
    if (GET_PRIVATE(promise, promiseStatus) === 0) {
84 85
      var tasks = GET_PRIVATE(promise, promiseQueue);
      if (tasks.length) PromiseEnqueue(value, tasks, status);
86 87 88
      PromiseSet(promise, status, value);
    }
  }
89

90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
  function PromiseCoerce(constructor, x) {
    if (!IsPromise(x) && IS_SPEC_OBJECT(x)) {
      var then;
      try {
        then = x.then;
      } catch(r) {
        return %_CallFunction(constructor, r, PromiseRejected);
      }
      if (IS_SPEC_FUNCTION(then)) {
        var deferred = %_CallFunction(constructor, PromiseDeferred);
        try {
          %_CallFunction(x, deferred.resolve, deferred.reject, then);
        } catch(r) {
          deferred.reject(r);
        }
        return deferred.promise;
      }
    }
    return x;
  }
110

111 112
  function PromiseHandle(value, handler, deferred) {
    try {
113
      %DebugPushPromise(deferred.promise);
114
      DEBUG_PREPARE_STEP_IN_IF_STEPPING(handler);
115 116 117 118 119 120 121 122
      var result = handler(value);
      if (result === deferred.promise)
        throw MakeTypeError('promise_cyclic', [result]);
      else if (IsPromise(result))
        %_CallFunction(result, deferred.resolve, deferred.reject, PromiseChain);
      else
        deferred.resolve(result);
    } catch (exception) {
123
      try { deferred.reject(exception); } catch (e) { }
124
    } finally {
125
      %DebugPopPromise();
126 127 128
    }
  }

129 130
  function PromiseEnqueue(value, tasks, status) {
    var id, name, instrumenting = DEBUG_IS_ACTIVE;
131
    %EnqueueMicrotask(function() {
132 133 134
      if (instrumenting) {
        %DebugAsyncTaskEvent({ type: "willHandle", id: id, name: name });
      }
135 136 137
      for (var i = 0; i < tasks.length; i += 2) {
        PromiseHandle(value, tasks[i], tasks[i + 1])
      }
138 139 140
      if (instrumenting) {
        %DebugAsyncTaskEvent({ type: "didHandle", id: id, name: name });
      }
141
    });
142 143
    if (instrumenting) {
      id = ++lastMicrotaskId;
144
      name = status > 0 ? "Promise.resolve" : "Promise.reject";
145 146
      %DebugAsyncTaskEvent({ type: "enqueue", id: id, name: name });
    }
147
  }
148

149 150
  function PromiseIdResolveHandler(x) { return x }
  function PromiseIdRejectHandler(r) { throw r }
151

152
  function PromiseNopResolver() {}
rossberg@chromium.org's avatar
rossberg@chromium.org committed
153

154 155 156 157 158 159
  // -------------------------------------------------------------------
  // Define exported functions.

  // For bootstrapper.

  IsPromise = function IsPromise(x) {
160
    return IS_SPEC_OBJECT(x) && HAS_DEFINED_PRIVATE(x, promiseStatus);
rossberg@chromium.org's avatar
rossberg@chromium.org committed
161
  }
162 163 164

  PromiseCreate = function PromiseCreate() {
    return new $Promise(PromiseNopResolver)
rossberg@chromium.org's avatar
rossberg@chromium.org committed
165
  }
166 167 168

  PromiseResolve = function PromiseResolve(promise, x) {
    PromiseDone(promise, +1, x, promiseOnResolve)
rossberg@chromium.org's avatar
rossberg@chromium.org committed
169 170
  }

171
  PromiseReject = function PromiseReject(promise, r) {
172
    // Check promise status to confirm that this reject has an effect.
173 174 175 176 177 178
    // Call runtime for callbacks to the debugger or for unhandled reject.
    if (GET_PRIVATE(promise, promiseStatus) == 0) {
      var debug_is_active = DEBUG_IS_ACTIVE;
      if (debug_is_active || !HAS_DEFINED_PRIVATE(promise, promiseHasHandler)) {
        %PromiseRejectEvent(promise, r, debug_is_active);
      }
179
    }
180 181
    PromiseDone(promise, -1, r, promiseOnReject)
  }
rossberg@chromium.org's avatar
rossberg@chromium.org committed
182

183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
  // 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;
201
    }
202 203 204 205 206
  }

  function PromiseResolved(x) {
    if (this === $Promise) {
      // Optimized case, avoid extra closure.
207
      return PromiseCreateAndSet(+1, x);
208 209
    } else {
      return new this(function(resolve, reject) { resolve(x) });
210
    }
rossberg@chromium.org's avatar
rossberg@chromium.org committed
211
  }
212 213

  function PromiseRejected(r) {
214
    var promise;
215 216
    if (this === $Promise) {
      // Optimized case, avoid extra closure.
217
      promise = PromiseCreateAndSet(-1, r);
218 219 220
      // The debug event for this would always be an uncaught promise reject,
      // which is usually simply noise. Do not trigger that debug event.
      %PromiseRejectEvent(promise, r, false);
221
    } else {
222
      promise = new this(function(resolve, reject) { reject(r) });
223
    }
224
    return promise;
225 226 227 228 229
  }

  // Simple chaining.

  PromiseChain = function PromiseChain(onResolve, onReject) {  // a.k.a.
230
                                                               // flatMap
231 232 233 234 235 236 237 238 239 240 241
    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
242 243 244
        PromiseEnqueue(GET_PRIVATE(this, promiseValue),
                       [onResolve, deferred],
                       +1);
245 246
        break;
      case -1:  // Rejected
247 248 249 250 251
        if (!HAS_DEFINED_PRIVATE(this, promiseHasHandler)) {
          // Promise has already been rejected, but had no handler.
          // Revoke previously triggered reject event.
          %PromiseRevokeReject(this);
        }
252 253 254
        PromiseEnqueue(GET_PRIVATE(this, promiseValue),
                       [onReject, deferred],
                       -1);
255
        break;
rossberg@chromium.org's avatar
rossberg@chromium.org committed
256
    }
257 258
    // Mark this promise as having handler.
    SET_PRIVATE(this, promiseHasHandler, true);
259
    if (DEBUG_IS_ACTIVE) {
260
      %DebugPromiseEvent({ promise: deferred.promise, parentPromise: this });
261
    }
262
    return deferred.promise;
rossberg@chromium.org's avatar
rossberg@chromium.org committed
263 264
  }

265 266 267
  PromiseCatch = function PromiseCatch(onReject) {
    return this.then(UNDEFINED, onReject);
  }
rossberg@chromium.org's avatar
rossberg@chromium.org committed
268

269 270
  // Multi-unwrapped chaining with thenable coercion.

271
  PromiseThen = function PromiseThen(onResolve, onReject) {
272 273 274 275 276 277 278 279 280 281
    onResolve = IS_SPEC_FUNCTION(onResolve) ? onResolve
                                            : PromiseIdResolveHandler;
    onReject = IS_SPEC_FUNCTION(onReject) ? onReject
                                          : PromiseIdRejectHandler;
    var that = this;
    var constructor = this.constructor;
    return %_CallFunction(
      this,
      function(x) {
        x = PromiseCoerce(constructor, x);
282 283 284 285 286 287 288 289 290
        if (x === that) {
          DEBUG_PREPARE_STEP_IN_IF_STEPPING(onReject);
          return onReject(MakeTypeError('promise_cyclic', [x]));
        } else if (IsPromise(x)) {
          return x.then(onResolve, onReject);
        } else {
          DEBUG_PREPARE_STEP_IN_IF_STEPPING(onResolve);
          return onResolve(x);
        }
291 292 293 294 295
      },
      onReject,
      PromiseChain
    );
  }
rossberg@chromium.org's avatar
rossberg@chromium.org committed
296

297
  // Combinators.
rossberg@chromium.org's avatar
rossberg@chromium.org committed
298

299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
  function PromiseCast(x) {
    // TODO(rossberg): cannot do better until we support @@create.
    return IsPromise(x) ? x : new this(function(resolve) { resolve(x) });
  }

  function PromiseAll(values) {
    var deferred = %_CallFunction(this, PromiseDeferred);
    var resolutions = [];
    if (!%_IsArray(values)) {
      deferred.reject(MakeTypeError('invalid_argument'));
      return deferred.promise;
    }
    try {
      var count = values.length;
      if (count === 0) {
        deferred.resolve(resolutions);
      } else {
        for (var i = 0; i < values.length; ++i) {
          this.resolve(values[i]).then(
318 319 320 321 322 323 324 325 326
            (function() {
              // Nested scope to get closure over current i (and avoid .bind).
              // TODO(rossberg): Use for-let instead once available.
              var i_captured = i;
              return function(x) {
                resolutions[i_captured] = x;
                if (--count === 0) deferred.resolve(resolutions);
              };
            })(),
327 328 329 330 331 332 333
            function(r) { deferred.reject(r) }
          );
        }
      }
    } catch (e) {
      deferred.reject(e)
    }
334 335
    return deferred.promise;
  }
336 337 338 339 340 341 342 343

  function PromiseOne(values) {
    var deferred = %_CallFunction(this, PromiseDeferred);
    if (!%_IsArray(values)) {
      deferred.reject(MakeTypeError('invalid_argument'));
      return deferred.promise;
    }
    try {
344
      for (var i = 0; i < values.length; ++i) {
345
        this.resolve(values[i]).then(
346
          function(x) { deferred.resolve(x) },
347 348 349
          function(r) { deferred.reject(r) }
        );
      }
350 351
    } catch (e) {
      deferred.reject(e)
rossberg@chromium.org's avatar
rossberg@chromium.org committed
352
    }
353 354
    return deferred.promise;
  }
rossberg@chromium.org's avatar
rossberg@chromium.org committed
355

356 357 358

  // Utility for debugger

359
  function PromiseHasUserDefinedRejectHandlerRecursive(promise) {
360 361 362 363
    var queue = GET_PRIVATE(promise, promiseOnReject);
    if (IS_UNDEFINED(queue)) return false;
    for (var i = 0; i < queue.length; i += 2) {
      if (queue[i] != PromiseIdRejectHandler) return true;
364 365 366
      if (PromiseHasUserDefinedRejectHandlerRecursive(queue[i + 1].promise)) {
        return true;
      }
367 368 369 370
    }
    return false;
  }

371 372 373 374 375 376
  // Return whether the promise will be handled by a user-defined reject
  // handler somewhere down the promise chain. For this, we do a depth-first
  // search for a reject handler that's not the default PromiseIdRejectHandler.
  PromiseHasUserDefinedRejectHandler =
      function PromiseHasUserDefinedRejectHandler() {
    return PromiseHasUserDefinedRejectHandlerRecursive(this);
377 378
  };

379 380
  // -------------------------------------------------------------------
  // Install exported functions.
rossberg@chromium.org's avatar
rossberg@chromium.org committed
381

382
  %CheckIsBootstrapping();
383
  %AddNamedProperty(global, 'Promise', $Promise, DONT_ENUM);
384 385
  %AddNamedProperty(
      $Promise.prototype, symbolToStringTag, "Promise", DONT_ENUM | READ_ONLY);
rossberg@chromium.org's avatar
rossberg@chromium.org committed
386
  InstallFunctions($Promise, DONT_ENUM, [
387
    "defer", PromiseDeferred,
388
    "accept", PromiseResolved,
389
    "reject", PromiseRejected,
rossberg@chromium.org's avatar
rossberg@chromium.org committed
390
    "all", PromiseAll,
391
    "race", PromiseOne,
392
    "resolve", PromiseCast
rossberg@chromium.org's avatar
rossberg@chromium.org committed
393 394 395 396 397 398 399
  ]);
  InstallFunctions($Promise.prototype, DONT_ENUM, [
    "chain", PromiseChain,
    "then", PromiseThen,
    "catch", PromiseCatch
  ]);

400
})();