|
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 "use strict"; |
|
8 |
|
9 this.EXPORTED_SYMBOLS = [ |
|
10 "Task" |
|
11 ]; |
|
12 |
|
13 /** |
|
14 * This module implements a subset of "Task.js" <http://taskjs.org/>. |
|
15 * |
|
16 * Paraphrasing from the Task.js site, tasks make sequential, asynchronous |
|
17 * operations simple, using the power of JavaScript's "yield" operator. |
|
18 * |
|
19 * Tasks are built upon generator functions and promises, documented here: |
|
20 * |
|
21 * <https://developer.mozilla.org/en/JavaScript/Guide/Iterators_and_Generators> |
|
22 * <http://wiki.commonjs.org/wiki/Promises/A> |
|
23 * |
|
24 * The "Task.spawn" function takes a generator function and starts running it as |
|
25 * a task. Every time the task yields a promise, it waits until the promise is |
|
26 * fulfilled. "Task.spawn" returns a promise that is resolved when the task |
|
27 * completes successfully, or is rejected if an exception occurs. |
|
28 * |
|
29 * ----------------------------------------------------------------------------- |
|
30 * |
|
31 * Cu.import("resource://gre/modules/Task.jsm"); |
|
32 * |
|
33 * Task.spawn(function* () { |
|
34 * |
|
35 * // This is our task. Let's create a promise object, wait on it and capture |
|
36 * // its resolution value. |
|
37 * let myPromise = getPromiseResolvedOnTimeoutWithValue(1000, "Value"); |
|
38 * let result = yield myPromise; |
|
39 * |
|
40 * // This part is executed only after the promise above is fulfilled (after |
|
41 * // one second, in this imaginary example). We can easily loop while |
|
42 * // calling asynchronous functions, and wait multiple times. |
|
43 * for (let i = 0; i < 3; i++) { |
|
44 * result += yield getPromiseResolvedOnTimeoutWithValue(50, "!"); |
|
45 * } |
|
46 * |
|
47 * return "Resolution result for the task: " + result; |
|
48 * }).then(function (result) { |
|
49 * |
|
50 * // result == "Resolution result for the task: Value!!!" |
|
51 * |
|
52 * // The result is undefined if no value was returned. |
|
53 * |
|
54 * }, function (exception) { |
|
55 * |
|
56 * // Failure! We can inspect or report the exception. |
|
57 * |
|
58 * }); |
|
59 * |
|
60 * ----------------------------------------------------------------------------- |
|
61 * |
|
62 * This module implements only the "Task.js" interfaces described above, with no |
|
63 * additional features to control the task externally, or do custom scheduling. |
|
64 * It also provides the following extensions that simplify task usage in the |
|
65 * most common cases: |
|
66 * |
|
67 * - The "Task.spawn" function also accepts an iterator returned by a generator |
|
68 * function, in addition to a generator function. This way, you can call into |
|
69 * the generator function with the parameters you want, and with "this" bound |
|
70 * to the correct value. Also, "this" is never bound to the task object when |
|
71 * "Task.spawn" calls the generator function. |
|
72 * |
|
73 * - In addition to a promise object, a task can yield the iterator returned by |
|
74 * a generator function. The iterator is turned into a task automatically. |
|
75 * This reduces the syntax overhead of calling "Task.spawn" explicitly when |
|
76 * you want to recurse into other task functions. |
|
77 * |
|
78 * - The "Task.spawn" function also accepts a primitive value, or a function |
|
79 * returning a primitive value, and treats the value as the result of the |
|
80 * task. This makes it possible to call an externally provided function and |
|
81 * spawn a task from it, regardless of whether it is an asynchronous generator |
|
82 * or a synchronous function. This comes in handy when iterating over |
|
83 * function lists where some items have been converted to tasks and some not. |
|
84 */ |
|
85 |
|
86 //////////////////////////////////////////////////////////////////////////////// |
|
87 //// Globals |
|
88 |
|
89 const Cc = Components.classes; |
|
90 const Ci = Components.interfaces; |
|
91 const Cu = Components.utils; |
|
92 const Cr = Components.results; |
|
93 |
|
94 Cu.import("resource://gre/modules/Promise.jsm"); |
|
95 |
|
96 // The following error types are considered programmer errors, which should be |
|
97 // reported (possibly redundantly) so as to let programmers fix their code. |
|
98 const ERRORS_TO_REPORT = ["EvalError", "RangeError", "ReferenceError", "TypeError"]; |
|
99 |
|
100 /** |
|
101 * Detect whether a value is a generator. |
|
102 * |
|
103 * @param aValue |
|
104 * The value to identify. |
|
105 * @return A boolean indicating whether the value is a generator. |
|
106 */ |
|
107 function isGenerator(aValue) { |
|
108 return Object.prototype.toString.call(aValue) == "[object Generator]"; |
|
109 } |
|
110 |
|
111 //////////////////////////////////////////////////////////////////////////////// |
|
112 //// Task |
|
113 |
|
114 /** |
|
115 * This object provides the public module functions. |
|
116 */ |
|
117 this.Task = { |
|
118 /** |
|
119 * Creates and starts a new task. |
|
120 * |
|
121 * @param aTask |
|
122 * - If you specify a generator function, it is called with no |
|
123 * arguments to retrieve the associated iterator. The generator |
|
124 * function is a task, that is can yield promise objects to wait |
|
125 * upon. |
|
126 * - If you specify the iterator returned by a generator function you |
|
127 * called, the generator function is also executed as a task. This |
|
128 * allows you to call the function with arguments. |
|
129 * - If you specify a function that is not a generator, it is called |
|
130 * with no arguments, and its return value is used to resolve the |
|
131 * returned promise. |
|
132 * - If you specify anything else, you get a promise that is already |
|
133 * resolved with the specified value. |
|
134 * |
|
135 * @return A promise object where you can register completion callbacks to be |
|
136 * called when the task terminates. |
|
137 */ |
|
138 spawn: function Task_spawn(aTask) { |
|
139 return createAsyncFunction(aTask).call(undefined); |
|
140 }, |
|
141 |
|
142 /** |
|
143 * Create and return an 'async function' that starts a new task. |
|
144 * |
|
145 * This is similar to 'spawn' except that it doesn't immediately start |
|
146 * the task, it binds the task to the async function's 'this' object and |
|
147 * arguments, and it requires the task to be a function. |
|
148 * |
|
149 * It simplifies the common pattern of implementing a method via a task, |
|
150 * like this simple object with a 'greet' method that has a 'name' parameter |
|
151 * and spawns a task to send a greeting and return its reply: |
|
152 * |
|
153 * let greeter = { |
|
154 * message: "Hello, NAME!", |
|
155 * greet: function(name) { |
|
156 * return Task.spawn((function* () { |
|
157 * return yield sendGreeting(this.message.replace(/NAME/, name)); |
|
158 * }).bind(this); |
|
159 * }) |
|
160 * }; |
|
161 * |
|
162 * With Task.async, the method can be declared succinctly: |
|
163 * |
|
164 * let greeter = { |
|
165 * message: "Hello, NAME!", |
|
166 * greet: Task.async(function* (name) { |
|
167 * return yield sendGreeting(this.message.replace(/NAME/, name)); |
|
168 * }) |
|
169 * }; |
|
170 * |
|
171 * While maintaining identical semantics: |
|
172 * |
|
173 * greeter.greet("Mitchell").then((reply) => { ... }); // behaves the same |
|
174 * |
|
175 * @param aTask |
|
176 * The task function to start. |
|
177 * |
|
178 * @return A function that starts the task function and returns its promise. |
|
179 */ |
|
180 async: function Task_async(aTask) { |
|
181 if (typeof(aTask) != "function") { |
|
182 throw new TypeError("aTask argument must be a function"); |
|
183 } |
|
184 |
|
185 return createAsyncFunction(aTask); |
|
186 }, |
|
187 |
|
188 /** |
|
189 * Constructs a special exception that, when thrown inside a legacy generator |
|
190 * function (non-star generator), allows the associated task to be resolved |
|
191 * with a specific value. |
|
192 * |
|
193 * Example: throw new Task.Result("Value"); |
|
194 */ |
|
195 Result: function Task_Result(aValue) { |
|
196 this.value = aValue; |
|
197 } |
|
198 }; |
|
199 |
|
200 function createAsyncFunction(aTask) { |
|
201 let asyncFunction = function () { |
|
202 let result = aTask; |
|
203 if (aTask && typeof(aTask) == "function") { |
|
204 if (aTask.isAsyncFunction) { |
|
205 throw new TypeError( |
|
206 "Cannot use an async function in place of a promise. " + |
|
207 "You should either invoke the async function first " + |
|
208 "or use 'Task.spawn' instead of 'Task.async' to start " + |
|
209 "the Task and return its promise."); |
|
210 } |
|
211 |
|
212 try { |
|
213 // Let's call into the function ourselves. |
|
214 result = aTask.apply(this, arguments); |
|
215 } catch (ex if ex instanceof Task.Result) { |
|
216 return Promise.resolve(ex.value); |
|
217 } catch (ex) { |
|
218 return Promise.reject(ex); |
|
219 } |
|
220 } |
|
221 |
|
222 if (isGenerator(result)) { |
|
223 // This is an iterator resulting from calling a generator function. |
|
224 return new TaskImpl(result).deferred.promise; |
|
225 } |
|
226 |
|
227 // Just propagate the given value to the caller as a resolved promise. |
|
228 return Promise.resolve(result); |
|
229 }; |
|
230 |
|
231 asyncFunction.isAsyncFunction = true; |
|
232 |
|
233 return asyncFunction; |
|
234 } |
|
235 |
|
236 //////////////////////////////////////////////////////////////////////////////// |
|
237 //// TaskImpl |
|
238 |
|
239 /** |
|
240 * Executes the specified iterator as a task, and gives access to the promise |
|
241 * that is fulfilled when the task terminates. |
|
242 */ |
|
243 function TaskImpl(iterator) { |
|
244 this.deferred = Promise.defer(); |
|
245 this._iterator = iterator; |
|
246 this._isStarGenerator = !("send" in iterator); |
|
247 this._run(true); |
|
248 } |
|
249 |
|
250 TaskImpl.prototype = { |
|
251 /** |
|
252 * Includes the promise object where task completion callbacks are registered, |
|
253 * and methods to resolve or reject the promise at task completion. |
|
254 */ |
|
255 deferred: null, |
|
256 |
|
257 /** |
|
258 * The iterator returned by the generator function associated with this task. |
|
259 */ |
|
260 _iterator: null, |
|
261 |
|
262 /** |
|
263 * Whether this Task is using a star generator. |
|
264 */ |
|
265 _isStarGenerator: false, |
|
266 |
|
267 /** |
|
268 * Main execution routine, that calls into the generator function. |
|
269 * |
|
270 * @param aSendResolved |
|
271 * If true, indicates that we should continue into the generator |
|
272 * function regularly (if we were waiting on a promise, it was |
|
273 * resolved). If true, indicates that we should cause an exception to |
|
274 * be thrown into the generator function (if we were waiting on a |
|
275 * promise, it was rejected). |
|
276 * @param aSendValue |
|
277 * Resolution result or rejection exception, if any. |
|
278 */ |
|
279 _run: function TaskImpl_run(aSendResolved, aSendValue) { |
|
280 if (this._isStarGenerator) { |
|
281 try { |
|
282 let result = aSendResolved ? this._iterator.next(aSendValue) |
|
283 : this._iterator.throw(aSendValue); |
|
284 |
|
285 if (result.done) { |
|
286 // The generator function returned. |
|
287 this.deferred.resolve(result.value); |
|
288 } else { |
|
289 // The generator function yielded. |
|
290 this._handleResultValue(result.value); |
|
291 } |
|
292 } catch (ex) { |
|
293 // The generator function failed with an uncaught exception. |
|
294 this._handleException(ex); |
|
295 } |
|
296 } else { |
|
297 try { |
|
298 let yielded = aSendResolved ? this._iterator.send(aSendValue) |
|
299 : this._iterator.throw(aSendValue); |
|
300 this._handleResultValue(yielded); |
|
301 } catch (ex if ex instanceof Task.Result) { |
|
302 // The generator function threw the special exception that allows it to |
|
303 // return a specific value on resolution. |
|
304 this.deferred.resolve(ex.value); |
|
305 } catch (ex if ex instanceof StopIteration) { |
|
306 // The generator function terminated with no specific result. |
|
307 this.deferred.resolve(); |
|
308 } catch (ex) { |
|
309 // The generator function failed with an uncaught exception. |
|
310 this._handleException(ex); |
|
311 } |
|
312 } |
|
313 }, |
|
314 |
|
315 /** |
|
316 * Handle a value yielded by a generator. |
|
317 * |
|
318 * @param aValue |
|
319 * The yielded value to handle. |
|
320 */ |
|
321 _handleResultValue: function TaskImpl_handleResultValue(aValue) { |
|
322 // If our task yielded an iterator resulting from calling another |
|
323 // generator function, automatically spawn a task from it, effectively |
|
324 // turning it into a promise that is fulfilled on task completion. |
|
325 if (isGenerator(aValue)) { |
|
326 aValue = Task.spawn(aValue); |
|
327 } |
|
328 |
|
329 if (aValue && typeof(aValue.then) == "function") { |
|
330 // We have a promise object now. When fulfilled, call again into this |
|
331 // function to continue the task, with either a resolution or rejection |
|
332 // condition. |
|
333 aValue.then(this._run.bind(this, true), |
|
334 this._run.bind(this, false)); |
|
335 } else { |
|
336 // If our task yielded a value that is not a promise, just continue and |
|
337 // pass it directly as the result of the yield statement. |
|
338 this._run(true, aValue); |
|
339 } |
|
340 }, |
|
341 |
|
342 /** |
|
343 * Handle an uncaught exception thrown from a generator. |
|
344 * |
|
345 * @param aException |
|
346 * The uncaught exception to handle. |
|
347 */ |
|
348 _handleException: function TaskImpl_handleException(aException) { |
|
349 if (aException && typeof aException == "object" && "name" in aException && |
|
350 ERRORS_TO_REPORT.indexOf(aException.name) != -1) { |
|
351 |
|
352 // We suspect that the exception is a programmer error, so we now |
|
353 // display it using dump(). Note that we do not use Cu.reportError as |
|
354 // we assume that this is a programming error, so we do not want end |
|
355 // users to see it. Also, if the programmer handles errors correctly, |
|
356 // they will either treat the error or log them somewhere. |
|
357 |
|
358 let stack = ("stack" in aException) ? aException.stack : "not available"; |
|
359 dump("*************************\n"); |
|
360 dump("A coding exception was thrown and uncaught in a Task.\n\n"); |
|
361 dump("Full message: " + aException + "\n"); |
|
362 dump("Full stack: " + stack + "\n"); |
|
363 dump("*************************\n"); |
|
364 } |
|
365 |
|
366 this.deferred.reject(aException); |
|
367 } |
|
368 }; |