1// Copyright 2012 the V8 project authors. All rights reserved. 2// Redistribution and use in source and binary forms, with or without 3// modification, are permitted provided that the following conditions are 4// met: 5// 6// * Redistributions of source code must retain the above copyright 7// notice, this list of conditions and the following disclaimer. 8// * Redistributions in binary form must reproduce the above 9// copyright notice, this list of conditions and the following 10// disclaimer in the documentation and/or other materials provided 11// with the distribution. 12// * Neither the name of Google Inc. nor the names of its 13// contributors may be used to endorse or promote products derived 14// from this software without specific prior written permission. 15// 16// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 28 29"use strict"; 30 31// This file relies on the fact that the following declaration has been made 32// in runtime.js: 33// var $Object = global.Object 34// var $WeakMap = global.WeakMap 35 36 37var $Promise = Promise; 38 39 40//------------------------------------------------------------------- 41 42// Core functionality. 43 44// Event queue format: [(value, [(handler, deferred)*])*] 45// I.e., a list of value/tasks pairs, where the value is a resolution value or 46// rejection reason, and the tasks are a respective list of handler/deferred 47// pairs waiting for notification of this value. Each handler is an onResolve or 48// onReject function provided to the same call of 'chain' that produced the 49// associated deferred. 50var promiseEvents = new InternalArray; 51 52// Status values: 0 = pending, +1 = resolved, -1 = rejected 53var promiseStatus = NEW_PRIVATE("Promise#status"); 54var promiseValue = NEW_PRIVATE("Promise#value"); 55var promiseOnResolve = NEW_PRIVATE("Promise#onResolve"); 56var promiseOnReject = NEW_PRIVATE("Promise#onReject"); 57var promiseRaw = NEW_PRIVATE("Promise#raw"); 58 59function IsPromise(x) { 60 return IS_SPEC_OBJECT(x) && %HasLocalProperty(x, promiseStatus); 61} 62 63function Promise(resolver) { 64 if (resolver === promiseRaw) return; 65 var promise = PromiseInit(this); 66 resolver(function(x) { PromiseResolve(promise, x) }, 67 function(r) { PromiseReject(promise, r) }); 68 // TODO(rossberg): current draft makes exception from this call asynchronous, 69 // but that's probably a mistake. 70} 71 72function PromiseSet(promise, status, value, onResolve, onReject) { 73 SET_PRIVATE(promise, promiseStatus, status); 74 SET_PRIVATE(promise, promiseValue, value); 75 SET_PRIVATE(promise, promiseOnResolve, onResolve); 76 SET_PRIVATE(promise, promiseOnReject, onReject); 77 return promise; 78} 79 80function PromiseInit(promise) { 81 return PromiseSet(promise, 0, UNDEFINED, new InternalArray, new InternalArray) 82} 83 84function PromiseDone(promise, status, value, promiseQueue) { 85 if (GET_PRIVATE(promise, promiseStatus) !== 0) return; 86 PromiseEnqueue(value, GET_PRIVATE(promise, promiseQueue)); 87 PromiseSet(promise, status, value); 88} 89 90function PromiseResolve(promise, x) { 91 PromiseDone(promise, +1, x, promiseOnResolve) 92} 93 94function PromiseReject(promise, r) { 95 PromiseDone(promise, -1, r, promiseOnReject) 96} 97 98 99// Convenience. 100 101function PromiseDeferred() { 102 if (this === $Promise) { 103 // Optimized case, avoid extra closure. 104 var promise = PromiseInit(new Promise(promiseRaw)); 105 return { 106 promise: promise, 107 resolve: function(x) { PromiseResolve(promise, x) }, 108 reject: function(r) { PromiseReject(promise, r) } 109 }; 110 } else { 111 var result = {}; 112 result.promise = new this(function(resolve, reject) { 113 result.resolve = resolve; 114 result.reject = reject; 115 }) 116 return result; 117 } 118} 119 120function PromiseResolved(x) { 121 if (this === $Promise) { 122 // Optimized case, avoid extra closure. 123 return PromiseSet(new Promise(promiseRaw), +1, x); 124 } else { 125 return new this(function(resolve, reject) { resolve(x) }); 126 } 127} 128 129function PromiseRejected(r) { 130 if (this === $Promise) { 131 // Optimized case, avoid extra closure. 132 return PromiseSet(new Promise(promiseRaw), -1, r); 133 } else { 134 return new this(function(resolve, reject) { reject(r) }); 135 } 136} 137 138 139// Simple chaining. 140 141function PromiseIdResolveHandler(x) { return x } 142function PromiseIdRejectHandler(r) { throw r } 143 144function PromiseChain(onResolve, onReject) { // a.k.a. flatMap 145 onResolve = IS_UNDEFINED(onResolve) ? PromiseIdResolveHandler : onResolve; 146 onReject = IS_UNDEFINED(onReject) ? PromiseIdRejectHandler : onReject; 147 var deferred = %_CallFunction(this.constructor, PromiseDeferred); 148 switch (GET_PRIVATE(this, promiseStatus)) { 149 case UNDEFINED: 150 throw MakeTypeError('not_a_promise', [this]); 151 case 0: // Pending 152 GET_PRIVATE(this, promiseOnResolve).push(onResolve, deferred); 153 GET_PRIVATE(this, promiseOnReject).push(onReject, deferred); 154 break; 155 case +1: // Resolved 156 PromiseEnqueue(GET_PRIVATE(this, promiseValue), [onResolve, deferred]); 157 break; 158 case -1: // Rejected 159 PromiseEnqueue(GET_PRIVATE(this, promiseValue), [onReject, deferred]); 160 break; 161 } 162 return deferred.promise; 163} 164 165function PromiseCatch(onReject) { 166 return this.chain(UNDEFINED, onReject); 167} 168 169function PromiseEnqueue(value, tasks) { 170 promiseEvents.push(value, tasks); 171 %SetMicrotaskPending(true); 172} 173 174function PromiseMicrotaskRunner() { 175 var events = promiseEvents; 176 if (events.length > 0) { 177 promiseEvents = new InternalArray; 178 for (var i = 0; i < events.length; i += 2) { 179 var value = events[i]; 180 var tasks = events[i + 1]; 181 for (var j = 0; j < tasks.length; j += 2) { 182 var handler = tasks[j]; 183 var deferred = tasks[j + 1]; 184 try { 185 var result = handler(value); 186 if (result === deferred.promise) 187 throw MakeTypeError('promise_cyclic', [result]); 188 else if (IsPromise(result)) 189 result.chain(deferred.resolve, deferred.reject); 190 else 191 deferred.resolve(result); 192 } catch(e) { 193 // TODO(rossberg): perhaps log uncaught exceptions below. 194 try { deferred.reject(e) } catch(e) {} 195 } 196 } 197 } 198 } 199} 200RunMicrotasks.runners.push(PromiseMicrotaskRunner); 201 202 203// Multi-unwrapped chaining with thenable coercion. 204 205function PromiseThen(onResolve, onReject) { 206 onResolve = IS_UNDEFINED(onResolve) ? PromiseIdResolveHandler : onResolve; 207 var that = this; 208 var constructor = this.constructor; 209 return this.chain( 210 function(x) { 211 x = PromiseCoerce(constructor, x); 212 return x === that ? onReject(MakeTypeError('promise_cyclic', [x])) : 213 IsPromise(x) ? x.then(onResolve, onReject) : onResolve(x); 214 }, 215 onReject 216 ); 217} 218 219PromiseCoerce.table = new $WeakMap; 220 221function PromiseCoerce(constructor, x) { 222 var then; 223 if (IsPromise(x)) { 224 return x; 225 } else if (!IS_NULL_OR_UNDEFINED(x) && %IsCallable(then = x.then)) { 226 if (PromiseCoerce.table.has(x)) { 227 return PromiseCoerce.table.get(x); 228 } else { 229 var deferred = constructor.deferred(); 230 PromiseCoerce.table.set(x, deferred.promise); 231 try { 232 %_CallFunction(x, deferred.resolve, deferred.reject, then); 233 } catch(e) { 234 deferred.reject(e); 235 } 236 return deferred.promise; 237 } 238 } else { 239 return x; 240 } 241} 242 243 244// Combinators. 245 246function PromiseCast(x) { 247 // TODO(rossberg): cannot do better until we support @@create. 248 return IsPromise(x) ? x : this.resolved(x); 249} 250 251function PromiseAll(values) { 252 var deferred = this.deferred(); 253 var resolutions = []; 254 var count = values.length; 255 if (count === 0) { 256 deferred.resolve(resolutions); 257 } else { 258 for (var i = 0; i < values.length; ++i) { 259 this.cast(values[i]).chain( 260 function(i, x) { 261 resolutions[i] = x; 262 if (--count === 0) deferred.resolve(resolutions); 263 }.bind(UNDEFINED, i), // TODO(rossberg): use let loop once available 264 function(r) { 265 if (count > 0) { count = 0; deferred.reject(r) } 266 } 267 ); 268 } 269 } 270 return deferred.promise; 271} 272 273function PromiseOne(values) { // a.k.a. race 274 var deferred = this.deferred(); 275 var done = false; 276 for (var i = 0; i < values.length; ++i) { 277 this.cast(values[i]).chain( 278 function(x) { if (!done) { done = true; deferred.resolve(x) } }, 279 function(r) { if (!done) { done = true; deferred.reject(r) } } 280 ); 281 } 282 return deferred.promise; 283} 284 285//------------------------------------------------------------------- 286 287function SetUpPromise() { 288 %CheckIsBootstrapping() 289 global.Promise = $Promise; 290 InstallFunctions($Promise, DONT_ENUM, [ 291 "deferred", PromiseDeferred, 292 "resolved", PromiseResolved, 293 "rejected", PromiseRejected, 294 "all", PromiseAll, 295 "one", PromiseOne, 296 "cast", PromiseCast 297 ]); 298 InstallFunctions($Promise.prototype, DONT_ENUM, [ 299 "chain", PromiseChain, 300 "then", PromiseThen, 301 "catch", PromiseCatch 302 ]); 303} 304 305SetUpPromise(); 306