c096296b0eb2b48696dc2490e70c2f7243cad653
[platform/upstream/nodejs.git] / deps / v8 / src / promise.js
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
14 var IsPromise;
15 var PromiseCreate;
16 var PromiseResolve;
17 var PromiseReject;
18 var PromiseChain;
19 var PromiseCatch;
20 var PromiseThen;
21 var PromiseHasRejectHandler;
22 var PromiseHasUserDefinedRejectHandler;
23
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
29 var promiseStatus = GLOBAL_PRIVATE("Promise#status");
30 var promiseValue = GLOBAL_PRIVATE("Promise#value");
31 var promiseOnResolve = GLOBAL_PRIVATE("Promise#onResolve");
32 var promiseOnReject = GLOBAL_PRIVATE("Promise#onReject");
33 var promiseRaw = GLOBAL_PRIVATE("Promise#raw");
34 var promiseHasHandler = %PromiseHasHandlerSymbol();
35 var lastMicrotaskId = 0;
36
37
38 (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
70   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
77   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) {
84       var tasks = GET_PRIVATE(promise, promiseQueue);
85       if (tasks.length) PromiseEnqueue(value, tasks, status);
86       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);
114       DEBUG_PREPARE_STEP_IN_IF_STEPPING(handler);
115       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.
173     // 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       }
179     }
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.
207       return PromiseCreateAndSet(+1, x);
208     } else {
209       return new this(function(resolve, reject) { resolve(x) });
210     }
211   }
212
213   function PromiseRejected(r) {
214     var promise;
215     if (this === $Promise) {
216       // Optimized case, avoid extra closure.
217       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);
221     } else {
222       promise = new this(function(resolve, reject) { reject(r) });
223     }
224     return promise;
225   }
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
247         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         }
252         PromiseEnqueue(GET_PRIVATE(this, promiseValue),
253                        [onReject, deferred],
254                        -1);
255         break;
256     }
257     // Mark this promise as having handler.
258     SET_PRIVATE(this, promiseHasHandler, true);
259     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);
282         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         }
291       },
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
359   function PromiseHasUserDefinedRejectHandlerRecursive(promise) {
360     var queue = GET_PRIVATE(promise, promiseOnReject);
361     if (IS_UNDEFINED(queue)) return false;
362     for (var i = 0; i < queue.length; i += 2) {
363       if (queue[i] != PromiseIdRejectHandler) return true;
364       if (PromiseHasUserDefinedRejectHandlerRecursive(queue[i + 1].promise)) {
365         return true;
366       }
367     }
368     return false;
369   }
370
371   // 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);
377   };
378
379   // -------------------------------------------------------------------
380   // Install exported functions.
381
382   %CheckIsBootstrapping();
383   %AddNamedProperty(global, 'Promise', $Promise, DONT_ENUM);
384   %AddNamedProperty(
385       $Promise.prototype, symbolToStringTag, "Promise", DONT_ENUM | READ_ONLY);
386   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 })();