1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/modules/Task.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,368 @@ 1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.8 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +"use strict"; 1.11 + 1.12 +this.EXPORTED_SYMBOLS = [ 1.13 + "Task" 1.14 +]; 1.15 + 1.16 +/** 1.17 + * This module implements a subset of "Task.js" <http://taskjs.org/>. 1.18 + * 1.19 + * Paraphrasing from the Task.js site, tasks make sequential, asynchronous 1.20 + * operations simple, using the power of JavaScript's "yield" operator. 1.21 + * 1.22 + * Tasks are built upon generator functions and promises, documented here: 1.23 + * 1.24 + * <https://developer.mozilla.org/en/JavaScript/Guide/Iterators_and_Generators> 1.25 + * <http://wiki.commonjs.org/wiki/Promises/A> 1.26 + * 1.27 + * The "Task.spawn" function takes a generator function and starts running it as 1.28 + * a task. Every time the task yields a promise, it waits until the promise is 1.29 + * fulfilled. "Task.spawn" returns a promise that is resolved when the task 1.30 + * completes successfully, or is rejected if an exception occurs. 1.31 + * 1.32 + * ----------------------------------------------------------------------------- 1.33 + * 1.34 + * Cu.import("resource://gre/modules/Task.jsm"); 1.35 + * 1.36 + * Task.spawn(function* () { 1.37 + * 1.38 + * // This is our task. Let's create a promise object, wait on it and capture 1.39 + * // its resolution value. 1.40 + * let myPromise = getPromiseResolvedOnTimeoutWithValue(1000, "Value"); 1.41 + * let result = yield myPromise; 1.42 + * 1.43 + * // This part is executed only after the promise above is fulfilled (after 1.44 + * // one second, in this imaginary example). We can easily loop while 1.45 + * // calling asynchronous functions, and wait multiple times. 1.46 + * for (let i = 0; i < 3; i++) { 1.47 + * result += yield getPromiseResolvedOnTimeoutWithValue(50, "!"); 1.48 + * } 1.49 + * 1.50 + * return "Resolution result for the task: " + result; 1.51 + * }).then(function (result) { 1.52 + * 1.53 + * // result == "Resolution result for the task: Value!!!" 1.54 + * 1.55 + * // The result is undefined if no value was returned. 1.56 + * 1.57 + * }, function (exception) { 1.58 + * 1.59 + * // Failure! We can inspect or report the exception. 1.60 + * 1.61 + * }); 1.62 + * 1.63 + * ----------------------------------------------------------------------------- 1.64 + * 1.65 + * This module implements only the "Task.js" interfaces described above, with no 1.66 + * additional features to control the task externally, or do custom scheduling. 1.67 + * It also provides the following extensions that simplify task usage in the 1.68 + * most common cases: 1.69 + * 1.70 + * - The "Task.spawn" function also accepts an iterator returned by a generator 1.71 + * function, in addition to a generator function. This way, you can call into 1.72 + * the generator function with the parameters you want, and with "this" bound 1.73 + * to the correct value. Also, "this" is never bound to the task object when 1.74 + * "Task.spawn" calls the generator function. 1.75 + * 1.76 + * - In addition to a promise object, a task can yield the iterator returned by 1.77 + * a generator function. The iterator is turned into a task automatically. 1.78 + * This reduces the syntax overhead of calling "Task.spawn" explicitly when 1.79 + * you want to recurse into other task functions. 1.80 + * 1.81 + * - The "Task.spawn" function also accepts a primitive value, or a function 1.82 + * returning a primitive value, and treats the value as the result of the 1.83 + * task. This makes it possible to call an externally provided function and 1.84 + * spawn a task from it, regardless of whether it is an asynchronous generator 1.85 + * or a synchronous function. This comes in handy when iterating over 1.86 + * function lists where some items have been converted to tasks and some not. 1.87 + */ 1.88 + 1.89 +//////////////////////////////////////////////////////////////////////////////// 1.90 +//// Globals 1.91 + 1.92 +const Cc = Components.classes; 1.93 +const Ci = Components.interfaces; 1.94 +const Cu = Components.utils; 1.95 +const Cr = Components.results; 1.96 + 1.97 +Cu.import("resource://gre/modules/Promise.jsm"); 1.98 + 1.99 +// The following error types are considered programmer errors, which should be 1.100 +// reported (possibly redundantly) so as to let programmers fix their code. 1.101 +const ERRORS_TO_REPORT = ["EvalError", "RangeError", "ReferenceError", "TypeError"]; 1.102 + 1.103 +/** 1.104 + * Detect whether a value is a generator. 1.105 + * 1.106 + * @param aValue 1.107 + * The value to identify. 1.108 + * @return A boolean indicating whether the value is a generator. 1.109 + */ 1.110 +function isGenerator(aValue) { 1.111 + return Object.prototype.toString.call(aValue) == "[object Generator]"; 1.112 +} 1.113 + 1.114 +//////////////////////////////////////////////////////////////////////////////// 1.115 +//// Task 1.116 + 1.117 +/** 1.118 + * This object provides the public module functions. 1.119 + */ 1.120 +this.Task = { 1.121 + /** 1.122 + * Creates and starts a new task. 1.123 + * 1.124 + * @param aTask 1.125 + * - If you specify a generator function, it is called with no 1.126 + * arguments to retrieve the associated iterator. The generator 1.127 + * function is a task, that is can yield promise objects to wait 1.128 + * upon. 1.129 + * - If you specify the iterator returned by a generator function you 1.130 + * called, the generator function is also executed as a task. This 1.131 + * allows you to call the function with arguments. 1.132 + * - If you specify a function that is not a generator, it is called 1.133 + * with no arguments, and its return value is used to resolve the 1.134 + * returned promise. 1.135 + * - If you specify anything else, you get a promise that is already 1.136 + * resolved with the specified value. 1.137 + * 1.138 + * @return A promise object where you can register completion callbacks to be 1.139 + * called when the task terminates. 1.140 + */ 1.141 + spawn: function Task_spawn(aTask) { 1.142 + return createAsyncFunction(aTask).call(undefined); 1.143 + }, 1.144 + 1.145 + /** 1.146 + * Create and return an 'async function' that starts a new task. 1.147 + * 1.148 + * This is similar to 'spawn' except that it doesn't immediately start 1.149 + * the task, it binds the task to the async function's 'this' object and 1.150 + * arguments, and it requires the task to be a function. 1.151 + * 1.152 + * It simplifies the common pattern of implementing a method via a task, 1.153 + * like this simple object with a 'greet' method that has a 'name' parameter 1.154 + * and spawns a task to send a greeting and return its reply: 1.155 + * 1.156 + * let greeter = { 1.157 + * message: "Hello, NAME!", 1.158 + * greet: function(name) { 1.159 + * return Task.spawn((function* () { 1.160 + * return yield sendGreeting(this.message.replace(/NAME/, name)); 1.161 + * }).bind(this); 1.162 + * }) 1.163 + * }; 1.164 + * 1.165 + * With Task.async, the method can be declared succinctly: 1.166 + * 1.167 + * let greeter = { 1.168 + * message: "Hello, NAME!", 1.169 + * greet: Task.async(function* (name) { 1.170 + * return yield sendGreeting(this.message.replace(/NAME/, name)); 1.171 + * }) 1.172 + * }; 1.173 + * 1.174 + * While maintaining identical semantics: 1.175 + * 1.176 + * greeter.greet("Mitchell").then((reply) => { ... }); // behaves the same 1.177 + * 1.178 + * @param aTask 1.179 + * The task function to start. 1.180 + * 1.181 + * @return A function that starts the task function and returns its promise. 1.182 + */ 1.183 + async: function Task_async(aTask) { 1.184 + if (typeof(aTask) != "function") { 1.185 + throw new TypeError("aTask argument must be a function"); 1.186 + } 1.187 + 1.188 + return createAsyncFunction(aTask); 1.189 + }, 1.190 + 1.191 + /** 1.192 + * Constructs a special exception that, when thrown inside a legacy generator 1.193 + * function (non-star generator), allows the associated task to be resolved 1.194 + * with a specific value. 1.195 + * 1.196 + * Example: throw new Task.Result("Value"); 1.197 + */ 1.198 + Result: function Task_Result(aValue) { 1.199 + this.value = aValue; 1.200 + } 1.201 +}; 1.202 + 1.203 +function createAsyncFunction(aTask) { 1.204 + let asyncFunction = function () { 1.205 + let result = aTask; 1.206 + if (aTask && typeof(aTask) == "function") { 1.207 + if (aTask.isAsyncFunction) { 1.208 + throw new TypeError( 1.209 + "Cannot use an async function in place of a promise. " + 1.210 + "You should either invoke the async function first " + 1.211 + "or use 'Task.spawn' instead of 'Task.async' to start " + 1.212 + "the Task and return its promise."); 1.213 + } 1.214 + 1.215 + try { 1.216 + // Let's call into the function ourselves. 1.217 + result = aTask.apply(this, arguments); 1.218 + } catch (ex if ex instanceof Task.Result) { 1.219 + return Promise.resolve(ex.value); 1.220 + } catch (ex) { 1.221 + return Promise.reject(ex); 1.222 + } 1.223 + } 1.224 + 1.225 + if (isGenerator(result)) { 1.226 + // This is an iterator resulting from calling a generator function. 1.227 + return new TaskImpl(result).deferred.promise; 1.228 + } 1.229 + 1.230 + // Just propagate the given value to the caller as a resolved promise. 1.231 + return Promise.resolve(result); 1.232 + }; 1.233 + 1.234 + asyncFunction.isAsyncFunction = true; 1.235 + 1.236 + return asyncFunction; 1.237 +} 1.238 + 1.239 +//////////////////////////////////////////////////////////////////////////////// 1.240 +//// TaskImpl 1.241 + 1.242 +/** 1.243 + * Executes the specified iterator as a task, and gives access to the promise 1.244 + * that is fulfilled when the task terminates. 1.245 + */ 1.246 +function TaskImpl(iterator) { 1.247 + this.deferred = Promise.defer(); 1.248 + this._iterator = iterator; 1.249 + this._isStarGenerator = !("send" in iterator); 1.250 + this._run(true); 1.251 +} 1.252 + 1.253 +TaskImpl.prototype = { 1.254 + /** 1.255 + * Includes the promise object where task completion callbacks are registered, 1.256 + * and methods to resolve or reject the promise at task completion. 1.257 + */ 1.258 + deferred: null, 1.259 + 1.260 + /** 1.261 + * The iterator returned by the generator function associated with this task. 1.262 + */ 1.263 + _iterator: null, 1.264 + 1.265 + /** 1.266 + * Whether this Task is using a star generator. 1.267 + */ 1.268 + _isStarGenerator: false, 1.269 + 1.270 + /** 1.271 + * Main execution routine, that calls into the generator function. 1.272 + * 1.273 + * @param aSendResolved 1.274 + * If true, indicates that we should continue into the generator 1.275 + * function regularly (if we were waiting on a promise, it was 1.276 + * resolved). If true, indicates that we should cause an exception to 1.277 + * be thrown into the generator function (if we were waiting on a 1.278 + * promise, it was rejected). 1.279 + * @param aSendValue 1.280 + * Resolution result or rejection exception, if any. 1.281 + */ 1.282 + _run: function TaskImpl_run(aSendResolved, aSendValue) { 1.283 + if (this._isStarGenerator) { 1.284 + try { 1.285 + let result = aSendResolved ? this._iterator.next(aSendValue) 1.286 + : this._iterator.throw(aSendValue); 1.287 + 1.288 + if (result.done) { 1.289 + // The generator function returned. 1.290 + this.deferred.resolve(result.value); 1.291 + } else { 1.292 + // The generator function yielded. 1.293 + this._handleResultValue(result.value); 1.294 + } 1.295 + } catch (ex) { 1.296 + // The generator function failed with an uncaught exception. 1.297 + this._handleException(ex); 1.298 + } 1.299 + } else { 1.300 + try { 1.301 + let yielded = aSendResolved ? this._iterator.send(aSendValue) 1.302 + : this._iterator.throw(aSendValue); 1.303 + this._handleResultValue(yielded); 1.304 + } catch (ex if ex instanceof Task.Result) { 1.305 + // The generator function threw the special exception that allows it to 1.306 + // return a specific value on resolution. 1.307 + this.deferred.resolve(ex.value); 1.308 + } catch (ex if ex instanceof StopIteration) { 1.309 + // The generator function terminated with no specific result. 1.310 + this.deferred.resolve(); 1.311 + } catch (ex) { 1.312 + // The generator function failed with an uncaught exception. 1.313 + this._handleException(ex); 1.314 + } 1.315 + } 1.316 + }, 1.317 + 1.318 + /** 1.319 + * Handle a value yielded by a generator. 1.320 + * 1.321 + * @param aValue 1.322 + * The yielded value to handle. 1.323 + */ 1.324 + _handleResultValue: function TaskImpl_handleResultValue(aValue) { 1.325 + // If our task yielded an iterator resulting from calling another 1.326 + // generator function, automatically spawn a task from it, effectively 1.327 + // turning it into a promise that is fulfilled on task completion. 1.328 + if (isGenerator(aValue)) { 1.329 + aValue = Task.spawn(aValue); 1.330 + } 1.331 + 1.332 + if (aValue && typeof(aValue.then) == "function") { 1.333 + // We have a promise object now. When fulfilled, call again into this 1.334 + // function to continue the task, with either a resolution or rejection 1.335 + // condition. 1.336 + aValue.then(this._run.bind(this, true), 1.337 + this._run.bind(this, false)); 1.338 + } else { 1.339 + // If our task yielded a value that is not a promise, just continue and 1.340 + // pass it directly as the result of the yield statement. 1.341 + this._run(true, aValue); 1.342 + } 1.343 + }, 1.344 + 1.345 + /** 1.346 + * Handle an uncaught exception thrown from a generator. 1.347 + * 1.348 + * @param aException 1.349 + * The uncaught exception to handle. 1.350 + */ 1.351 + _handleException: function TaskImpl_handleException(aException) { 1.352 + if (aException && typeof aException == "object" && "name" in aException && 1.353 + ERRORS_TO_REPORT.indexOf(aException.name) != -1) { 1.354 + 1.355 + // We suspect that the exception is a programmer error, so we now 1.356 + // display it using dump(). Note that we do not use Cu.reportError as 1.357 + // we assume that this is a programming error, so we do not want end 1.358 + // users to see it. Also, if the programmer handles errors correctly, 1.359 + // they will either treat the error or log them somewhere. 1.360 + 1.361 + let stack = ("stack" in aException) ? aException.stack : "not available"; 1.362 + dump("*************************\n"); 1.363 + dump("A coding exception was thrown and uncaught in a Task.\n\n"); 1.364 + dump("Full message: " + aException + "\n"); 1.365 + dump("Full stack: " + stack + "\n"); 1.366 + dump("*************************\n"); 1.367 + } 1.368 + 1.369 + this.deferred.reject(aException); 1.370 + } 1.371 +};