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