blob: 37c10ec08eb298d5543738531168fe0315ca7854 [file] [log] [blame]
Ben Murdochb8a8cc12014-11-26 15:28:44 +00001// 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})();