blob: c096296b0eb2b48696dc2490e70c2f7243cad653 [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;
Emily Bernierd0a1eb72015-03-24 16:35:39 -040022var PromiseHasUserDefinedRejectHandler;
Ben Murdochb8a8cc12014-11-26 15:28:44 +000023
24// mirror-debugger.js currently uses builtins.promiseStatus. It would be nice
25// if we could move these property names into the closure below.
26// TODO(jkummerow/rossberg/yangguo): Find a better solution.
27
28// Status values: 0 = pending, +1 = resolved, -1 = rejected
29var promiseStatus = GLOBAL_PRIVATE("Promise#status");
30var promiseValue = GLOBAL_PRIVATE("Promise#value");
31var promiseOnResolve = GLOBAL_PRIVATE("Promise#onResolve");
32var promiseOnReject = GLOBAL_PRIVATE("Promise#onReject");
33var promiseRaw = GLOBAL_PRIVATE("Promise#raw");
Emily Bernierd0a1eb72015-03-24 16:35:39 -040034var promiseHasHandler = %PromiseHasHandlerSymbol();
Ben Murdochb8a8cc12014-11-26 15:28:44 +000035var lastMicrotaskId = 0;
36
Emily Bernierd0a1eb72015-03-24 16:35:39 -040037
Ben Murdochb8a8cc12014-11-26 15:28:44 +000038(function() {
39
40 var $Promise = function Promise(resolver) {
41 if (resolver === promiseRaw) return;
42 if (!%_IsConstructCall()) throw MakeTypeError('not_a_promise', [this]);
43 if (!IS_SPEC_FUNCTION(resolver))
44 throw MakeTypeError('resolver_not_a_function', [resolver]);
45 var promise = PromiseInit(this);
46 try {
47 %DebugPushPromise(promise);
48 resolver(function(x) { PromiseResolve(promise, x) },
49 function(r) { PromiseReject(promise, r) });
50 } catch (e) {
51 PromiseReject(promise, e);
52 } finally {
53 %DebugPopPromise();
54 }
55 }
56
57 // Core functionality.
58
59 function PromiseSet(promise, status, value, onResolve, onReject) {
60 SET_PRIVATE(promise, promiseStatus, status);
61 SET_PRIVATE(promise, promiseValue, value);
62 SET_PRIVATE(promise, promiseOnResolve, onResolve);
63 SET_PRIVATE(promise, promiseOnReject, onReject);
64 if (DEBUG_IS_ACTIVE) {
65 %DebugPromiseEvent({ promise: promise, status: status, value: value });
66 }
67 return promise;
68 }
69
Emily Bernierd0a1eb72015-03-24 16:35:39 -040070 function PromiseCreateAndSet(status, value) {
71 var promise = new $Promise(promiseRaw);
72 // If debug is active, notify about the newly created promise first.
73 if (DEBUG_IS_ACTIVE) PromiseSet(promise, 0, UNDEFINED);
74 return PromiseSet(promise, status, value);
75 }
76
Ben Murdochb8a8cc12014-11-26 15:28:44 +000077 function PromiseInit(promise) {
78 return PromiseSet(
79 promise, 0, UNDEFINED, new InternalArray, new InternalArray)
80 }
81
82 function PromiseDone(promise, status, value, promiseQueue) {
83 if (GET_PRIVATE(promise, promiseStatus) === 0) {
Emily Bernierd0a1eb72015-03-24 16:35:39 -040084 var tasks = GET_PRIVATE(promise, promiseQueue);
85 if (tasks.length) PromiseEnqueue(value, tasks, status);
Ben Murdochb8a8cc12014-11-26 15:28:44 +000086 PromiseSet(promise, status, value);
87 }
88 }
89
90 function PromiseCoerce(constructor, x) {
91 if (!IsPromise(x) && IS_SPEC_OBJECT(x)) {
92 var then;
93 try {
94 then = x.then;
95 } catch(r) {
96 return %_CallFunction(constructor, r, PromiseRejected);
97 }
98 if (IS_SPEC_FUNCTION(then)) {
99 var deferred = %_CallFunction(constructor, PromiseDeferred);
100 try {
101 %_CallFunction(x, deferred.resolve, deferred.reject, then);
102 } catch(r) {
103 deferred.reject(r);
104 }
105 return deferred.promise;
106 }
107 }
108 return x;
109 }
110
111 function PromiseHandle(value, handler, deferred) {
112 try {
113 %DebugPushPromise(deferred.promise);
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400114 DEBUG_PREPARE_STEP_IN_IF_STEPPING(handler);
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000115 var result = handler(value);
116 if (result === deferred.promise)
117 throw MakeTypeError('promise_cyclic', [result]);
118 else if (IsPromise(result))
119 %_CallFunction(result, deferred.resolve, deferred.reject, PromiseChain);
120 else
121 deferred.resolve(result);
122 } catch (exception) {
123 try { deferred.reject(exception); } catch (e) { }
124 } finally {
125 %DebugPopPromise();
126 }
127 }
128
129 function PromiseEnqueue(value, tasks, status) {
130 var id, name, instrumenting = DEBUG_IS_ACTIVE;
131 %EnqueueMicrotask(function() {
132 if (instrumenting) {
133 %DebugAsyncTaskEvent({ type: "willHandle", id: id, name: name });
134 }
135 for (var i = 0; i < tasks.length; i += 2) {
136 PromiseHandle(value, tasks[i], tasks[i + 1])
137 }
138 if (instrumenting) {
139 %DebugAsyncTaskEvent({ type: "didHandle", id: id, name: name });
140 }
141 });
142 if (instrumenting) {
143 id = ++lastMicrotaskId;
144 name = status > 0 ? "Promise.resolve" : "Promise.reject";
145 %DebugAsyncTaskEvent({ type: "enqueue", id: id, name: name });
146 }
147 }
148
149 function PromiseIdResolveHandler(x) { return x }
150 function PromiseIdRejectHandler(r) { throw r }
151
152 function PromiseNopResolver() {}
153
154 // -------------------------------------------------------------------
155 // Define exported functions.
156
157 // For bootstrapper.
158
159 IsPromise = function IsPromise(x) {
160 return IS_SPEC_OBJECT(x) && HAS_DEFINED_PRIVATE(x, promiseStatus);
161 }
162
163 PromiseCreate = function PromiseCreate() {
164 return new $Promise(PromiseNopResolver)
165 }
166
167 PromiseResolve = function PromiseResolve(promise, x) {
168 PromiseDone(promise, +1, x, promiseOnResolve)
169 }
170
171 PromiseReject = function PromiseReject(promise, r) {
172 // Check promise status to confirm that this reject has an effect.
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400173 // Call runtime for callbacks to the debugger or for unhandled reject.
174 if (GET_PRIVATE(promise, promiseStatus) == 0) {
175 var debug_is_active = DEBUG_IS_ACTIVE;
176 if (debug_is_active || !HAS_DEFINED_PRIVATE(promise, promiseHasHandler)) {
177 %PromiseRejectEvent(promise, r, debug_is_active);
178 }
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000179 }
180 PromiseDone(promise, -1, r, promiseOnReject)
181 }
182
183 // Convenience.
184
185 function PromiseDeferred() {
186 if (this === $Promise) {
187 // Optimized case, avoid extra closure.
188 var promise = PromiseInit(new $Promise(promiseRaw));
189 return {
190 promise: promise,
191 resolve: function(x) { PromiseResolve(promise, x) },
192 reject: function(r) { PromiseReject(promise, r) }
193 };
194 } else {
195 var result = {};
196 result.promise = new this(function(resolve, reject) {
197 result.resolve = resolve;
198 result.reject = reject;
199 })
200 return result;
201 }
202 }
203
204 function PromiseResolved(x) {
205 if (this === $Promise) {
206 // Optimized case, avoid extra closure.
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400207 return PromiseCreateAndSet(+1, x);
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000208 } else {
209 return new this(function(resolve, reject) { resolve(x) });
210 }
211 }
212
213 function PromiseRejected(r) {
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400214 var promise;
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000215 if (this === $Promise) {
216 // Optimized case, avoid extra closure.
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400217 promise = PromiseCreateAndSet(-1, r);
218 // The debug event for this would always be an uncaught promise reject,
219 // which is usually simply noise. Do not trigger that debug event.
220 %PromiseRejectEvent(promise, r, false);
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000221 } else {
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400222 promise = new this(function(resolve, reject) { reject(r) });
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000223 }
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400224 return promise;
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000225 }
226
227 // Simple chaining.
228
229 PromiseChain = function PromiseChain(onResolve, onReject) { // a.k.a.
230 // flatMap
231 onResolve = IS_UNDEFINED(onResolve) ? PromiseIdResolveHandler : onResolve;
232 onReject = IS_UNDEFINED(onReject) ? PromiseIdRejectHandler : onReject;
233 var deferred = %_CallFunction(this.constructor, PromiseDeferred);
234 switch (GET_PRIVATE(this, promiseStatus)) {
235 case UNDEFINED:
236 throw MakeTypeError('not_a_promise', [this]);
237 case 0: // Pending
238 GET_PRIVATE(this, promiseOnResolve).push(onResolve, deferred);
239 GET_PRIVATE(this, promiseOnReject).push(onReject, deferred);
240 break;
241 case +1: // Resolved
242 PromiseEnqueue(GET_PRIVATE(this, promiseValue),
243 [onResolve, deferred],
244 +1);
245 break;
246 case -1: // Rejected
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400247 if (!HAS_DEFINED_PRIVATE(this, promiseHasHandler)) {
248 // Promise has already been rejected, but had no handler.
249 // Revoke previously triggered reject event.
250 %PromiseRevokeReject(this);
251 }
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000252 PromiseEnqueue(GET_PRIVATE(this, promiseValue),
253 [onReject, deferred],
254 -1);
255 break;
256 }
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400257 // Mark this promise as having handler.
258 SET_PRIVATE(this, promiseHasHandler, true);
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000259 if (DEBUG_IS_ACTIVE) {
260 %DebugPromiseEvent({ promise: deferred.promise, parentPromise: this });
261 }
262 return deferred.promise;
263 }
264
265 PromiseCatch = function PromiseCatch(onReject) {
266 return this.then(UNDEFINED, onReject);
267 }
268
269 // Multi-unwrapped chaining with thenable coercion.
270
271 PromiseThen = function PromiseThen(onResolve, onReject) {
272 onResolve = IS_SPEC_FUNCTION(onResolve) ? onResolve
273 : PromiseIdResolveHandler;
274 onReject = IS_SPEC_FUNCTION(onReject) ? onReject
275 : PromiseIdRejectHandler;
276 var that = this;
277 var constructor = this.constructor;
278 return %_CallFunction(
279 this,
280 function(x) {
281 x = PromiseCoerce(constructor, x);
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400282 if (x === that) {
283 DEBUG_PREPARE_STEP_IN_IF_STEPPING(onReject);
284 return onReject(MakeTypeError('promise_cyclic', [x]));
285 } else if (IsPromise(x)) {
286 return x.then(onResolve, onReject);
287 } else {
288 DEBUG_PREPARE_STEP_IN_IF_STEPPING(onResolve);
289 return onResolve(x);
290 }
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000291 },
292 onReject,
293 PromiseChain
294 );
295 }
296
297 // Combinators.
298
299 function PromiseCast(x) {
300 // TODO(rossberg): cannot do better until we support @@create.
301 return IsPromise(x) ? x : new this(function(resolve) { resolve(x) });
302 }
303
304 function PromiseAll(values) {
305 var deferred = %_CallFunction(this, PromiseDeferred);
306 var resolutions = [];
307 if (!%_IsArray(values)) {
308 deferred.reject(MakeTypeError('invalid_argument'));
309 return deferred.promise;
310 }
311 try {
312 var count = values.length;
313 if (count === 0) {
314 deferred.resolve(resolutions);
315 } else {
316 for (var i = 0; i < values.length; ++i) {
317 this.resolve(values[i]).then(
318 (function() {
319 // Nested scope to get closure over current i (and avoid .bind).
320 // TODO(rossberg): Use for-let instead once available.
321 var i_captured = i;
322 return function(x) {
323 resolutions[i_captured] = x;
324 if (--count === 0) deferred.resolve(resolutions);
325 };
326 })(),
327 function(r) { deferred.reject(r) }
328 );
329 }
330 }
331 } catch (e) {
332 deferred.reject(e)
333 }
334 return deferred.promise;
335 }
336
337 function PromiseOne(values) {
338 var deferred = %_CallFunction(this, PromiseDeferred);
339 if (!%_IsArray(values)) {
340 deferred.reject(MakeTypeError('invalid_argument'));
341 return deferred.promise;
342 }
343 try {
344 for (var i = 0; i < values.length; ++i) {
345 this.resolve(values[i]).then(
346 function(x) { deferred.resolve(x) },
347 function(r) { deferred.reject(r) }
348 );
349 }
350 } catch (e) {
351 deferred.reject(e)
352 }
353 return deferred.promise;
354 }
355
356
357 // Utility for debugger
358
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400359 function PromiseHasUserDefinedRejectHandlerRecursive(promise) {
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000360 var queue = GET_PRIVATE(promise, promiseOnReject);
361 if (IS_UNDEFINED(queue)) return false;
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000362 for (var i = 0; i < queue.length; i += 2) {
363 if (queue[i] != PromiseIdRejectHandler) return true;
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400364 if (PromiseHasUserDefinedRejectHandlerRecursive(queue[i + 1].promise)) {
365 return true;
366 }
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000367 }
368 return false;
369 }
370
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400371 // Return whether the promise will be handled by a user-defined reject
372 // handler somewhere down the promise chain. For this, we do a depth-first
373 // search for a reject handler that's not the default PromiseIdRejectHandler.
374 PromiseHasUserDefinedRejectHandler =
375 function PromiseHasUserDefinedRejectHandler() {
376 return PromiseHasUserDefinedRejectHandlerRecursive(this);
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000377 };
378
379 // -------------------------------------------------------------------
380 // Install exported functions.
381
382 %CheckIsBootstrapping();
383 %AddNamedProperty(global, 'Promise', $Promise, DONT_ENUM);
Emily Bernierd0a1eb72015-03-24 16:35:39 -0400384 %AddNamedProperty(
385 $Promise.prototype, symbolToStringTag, "Promise", DONT_ENUM | READ_ONLY);
Ben Murdochb8a8cc12014-11-26 15:28:44 +0000386 InstallFunctions($Promise, DONT_ENUM, [
387 "defer", PromiseDeferred,
388 "accept", PromiseResolved,
389 "reject", PromiseRejected,
390 "all", PromiseAll,
391 "race", PromiseOne,
392 "resolve", PromiseCast
393 ]);
394 InstallFunctions($Promise.prototype, DONT_ENUM, [
395 "chain", PromiseChain,
396 "then", PromiseThen,
397 "catch", PromiseCatch
398 ]);
399
400})();