michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = [ michael@0: "Task" michael@0: ]; michael@0: michael@0: /** michael@0: * This module implements a subset of "Task.js" . michael@0: * michael@0: * Paraphrasing from the Task.js site, tasks make sequential, asynchronous michael@0: * operations simple, using the power of JavaScript's "yield" operator. michael@0: * michael@0: * Tasks are built upon generator functions and promises, documented here: michael@0: * michael@0: * michael@0: * michael@0: * michael@0: * The "Task.spawn" function takes a generator function and starts running it as michael@0: * a task. Every time the task yields a promise, it waits until the promise is michael@0: * fulfilled. "Task.spawn" returns a promise that is resolved when the task michael@0: * completes successfully, or is rejected if an exception occurs. michael@0: * michael@0: * ----------------------------------------------------------------------------- michael@0: * michael@0: * Cu.import("resource://gre/modules/Task.jsm"); michael@0: * michael@0: * Task.spawn(function* () { michael@0: * michael@0: * // This is our task. Let's create a promise object, wait on it and capture michael@0: * // its resolution value. michael@0: * let myPromise = getPromiseResolvedOnTimeoutWithValue(1000, "Value"); michael@0: * let result = yield myPromise; michael@0: * michael@0: * // This part is executed only after the promise above is fulfilled (after michael@0: * // one second, in this imaginary example). We can easily loop while michael@0: * // calling asynchronous functions, and wait multiple times. michael@0: * for (let i = 0; i < 3; i++) { michael@0: * result += yield getPromiseResolvedOnTimeoutWithValue(50, "!"); michael@0: * } michael@0: * michael@0: * return "Resolution result for the task: " + result; michael@0: * }).then(function (result) { michael@0: * michael@0: * // result == "Resolution result for the task: Value!!!" michael@0: * michael@0: * // The result is undefined if no value was returned. michael@0: * michael@0: * }, function (exception) { michael@0: * michael@0: * // Failure! We can inspect or report the exception. michael@0: * michael@0: * }); michael@0: * michael@0: * ----------------------------------------------------------------------------- michael@0: * michael@0: * This module implements only the "Task.js" interfaces described above, with no michael@0: * additional features to control the task externally, or do custom scheduling. michael@0: * It also provides the following extensions that simplify task usage in the michael@0: * most common cases: michael@0: * michael@0: * - The "Task.spawn" function also accepts an iterator returned by a generator michael@0: * function, in addition to a generator function. This way, you can call into michael@0: * the generator function with the parameters you want, and with "this" bound michael@0: * to the correct value. Also, "this" is never bound to the task object when michael@0: * "Task.spawn" calls the generator function. michael@0: * michael@0: * - In addition to a promise object, a task can yield the iterator returned by michael@0: * a generator function. The iterator is turned into a task automatically. michael@0: * This reduces the syntax overhead of calling "Task.spawn" explicitly when michael@0: * you want to recurse into other task functions. michael@0: * michael@0: * - The "Task.spawn" function also accepts a primitive value, or a function michael@0: * returning a primitive value, and treats the value as the result of the michael@0: * task. This makes it possible to call an externally provided function and michael@0: * spawn a task from it, regardless of whether it is an asynchronous generator michael@0: * or a synchronous function. This comes in handy when iterating over michael@0: * function lists where some items have been converted to tasks and some not. michael@0: */ michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Globals michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: const Cr = Components.results; michael@0: michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: michael@0: // The following error types are considered programmer errors, which should be michael@0: // reported (possibly redundantly) so as to let programmers fix their code. michael@0: const ERRORS_TO_REPORT = ["EvalError", "RangeError", "ReferenceError", "TypeError"]; michael@0: michael@0: /** michael@0: * Detect whether a value is a generator. michael@0: * michael@0: * @param aValue michael@0: * The value to identify. michael@0: * @return A boolean indicating whether the value is a generator. michael@0: */ michael@0: function isGenerator(aValue) { michael@0: return Object.prototype.toString.call(aValue) == "[object Generator]"; michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Task michael@0: michael@0: /** michael@0: * This object provides the public module functions. michael@0: */ michael@0: this.Task = { michael@0: /** michael@0: * Creates and starts a new task. michael@0: * michael@0: * @param aTask michael@0: * - If you specify a generator function, it is called with no michael@0: * arguments to retrieve the associated iterator. The generator michael@0: * function is a task, that is can yield promise objects to wait michael@0: * upon. michael@0: * - If you specify the iterator returned by a generator function you michael@0: * called, the generator function is also executed as a task. This michael@0: * allows you to call the function with arguments. michael@0: * - If you specify a function that is not a generator, it is called michael@0: * with no arguments, and its return value is used to resolve the michael@0: * returned promise. michael@0: * - If you specify anything else, you get a promise that is already michael@0: * resolved with the specified value. michael@0: * michael@0: * @return A promise object where you can register completion callbacks to be michael@0: * called when the task terminates. michael@0: */ michael@0: spawn: function Task_spawn(aTask) { michael@0: return createAsyncFunction(aTask).call(undefined); michael@0: }, michael@0: michael@0: /** michael@0: * Create and return an 'async function' that starts a new task. michael@0: * michael@0: * This is similar to 'spawn' except that it doesn't immediately start michael@0: * the task, it binds the task to the async function's 'this' object and michael@0: * arguments, and it requires the task to be a function. michael@0: * michael@0: * It simplifies the common pattern of implementing a method via a task, michael@0: * like this simple object with a 'greet' method that has a 'name' parameter michael@0: * and spawns a task to send a greeting and return its reply: michael@0: * michael@0: * let greeter = { michael@0: * message: "Hello, NAME!", michael@0: * greet: function(name) { michael@0: * return Task.spawn((function* () { michael@0: * return yield sendGreeting(this.message.replace(/NAME/, name)); michael@0: * }).bind(this); michael@0: * }) michael@0: * }; michael@0: * michael@0: * With Task.async, the method can be declared succinctly: michael@0: * michael@0: * let greeter = { michael@0: * message: "Hello, NAME!", michael@0: * greet: Task.async(function* (name) { michael@0: * return yield sendGreeting(this.message.replace(/NAME/, name)); michael@0: * }) michael@0: * }; michael@0: * michael@0: * While maintaining identical semantics: michael@0: * michael@0: * greeter.greet("Mitchell").then((reply) => { ... }); // behaves the same michael@0: * michael@0: * @param aTask michael@0: * The task function to start. michael@0: * michael@0: * @return A function that starts the task function and returns its promise. michael@0: */ michael@0: async: function Task_async(aTask) { michael@0: if (typeof(aTask) != "function") { michael@0: throw new TypeError("aTask argument must be a function"); michael@0: } michael@0: michael@0: return createAsyncFunction(aTask); michael@0: }, michael@0: michael@0: /** michael@0: * Constructs a special exception that, when thrown inside a legacy generator michael@0: * function (non-star generator), allows the associated task to be resolved michael@0: * with a specific value. michael@0: * michael@0: * Example: throw new Task.Result("Value"); michael@0: */ michael@0: Result: function Task_Result(aValue) { michael@0: this.value = aValue; michael@0: } michael@0: }; michael@0: michael@0: function createAsyncFunction(aTask) { michael@0: let asyncFunction = function () { michael@0: let result = aTask; michael@0: if (aTask && typeof(aTask) == "function") { michael@0: if (aTask.isAsyncFunction) { michael@0: throw new TypeError( michael@0: "Cannot use an async function in place of a promise. " + michael@0: "You should either invoke the async function first " + michael@0: "or use 'Task.spawn' instead of 'Task.async' to start " + michael@0: "the Task and return its promise."); michael@0: } michael@0: michael@0: try { michael@0: // Let's call into the function ourselves. michael@0: result = aTask.apply(this, arguments); michael@0: } catch (ex if ex instanceof Task.Result) { michael@0: return Promise.resolve(ex.value); michael@0: } catch (ex) { michael@0: return Promise.reject(ex); michael@0: } michael@0: } michael@0: michael@0: if (isGenerator(result)) { michael@0: // This is an iterator resulting from calling a generator function. michael@0: return new TaskImpl(result).deferred.promise; michael@0: } michael@0: michael@0: // Just propagate the given value to the caller as a resolved promise. michael@0: return Promise.resolve(result); michael@0: }; michael@0: michael@0: asyncFunction.isAsyncFunction = true; michael@0: michael@0: return asyncFunction; michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// TaskImpl michael@0: michael@0: /** michael@0: * Executes the specified iterator as a task, and gives access to the promise michael@0: * that is fulfilled when the task terminates. michael@0: */ michael@0: function TaskImpl(iterator) { michael@0: this.deferred = Promise.defer(); michael@0: this._iterator = iterator; michael@0: this._isStarGenerator = !("send" in iterator); michael@0: this._run(true); michael@0: } michael@0: michael@0: TaskImpl.prototype = { michael@0: /** michael@0: * Includes the promise object where task completion callbacks are registered, michael@0: * and methods to resolve or reject the promise at task completion. michael@0: */ michael@0: deferred: null, michael@0: michael@0: /** michael@0: * The iterator returned by the generator function associated with this task. michael@0: */ michael@0: _iterator: null, michael@0: michael@0: /** michael@0: * Whether this Task is using a star generator. michael@0: */ michael@0: _isStarGenerator: false, michael@0: michael@0: /** michael@0: * Main execution routine, that calls into the generator function. michael@0: * michael@0: * @param aSendResolved michael@0: * If true, indicates that we should continue into the generator michael@0: * function regularly (if we were waiting on a promise, it was michael@0: * resolved). If true, indicates that we should cause an exception to michael@0: * be thrown into the generator function (if we were waiting on a michael@0: * promise, it was rejected). michael@0: * @param aSendValue michael@0: * Resolution result or rejection exception, if any. michael@0: */ michael@0: _run: function TaskImpl_run(aSendResolved, aSendValue) { michael@0: if (this._isStarGenerator) { michael@0: try { michael@0: let result = aSendResolved ? this._iterator.next(aSendValue) michael@0: : this._iterator.throw(aSendValue); michael@0: michael@0: if (result.done) { michael@0: // The generator function returned. michael@0: this.deferred.resolve(result.value); michael@0: } else { michael@0: // The generator function yielded. michael@0: this._handleResultValue(result.value); michael@0: } michael@0: } catch (ex) { michael@0: // The generator function failed with an uncaught exception. michael@0: this._handleException(ex); michael@0: } michael@0: } else { michael@0: try { michael@0: let yielded = aSendResolved ? this._iterator.send(aSendValue) michael@0: : this._iterator.throw(aSendValue); michael@0: this._handleResultValue(yielded); michael@0: } catch (ex if ex instanceof Task.Result) { michael@0: // The generator function threw the special exception that allows it to michael@0: // return a specific value on resolution. michael@0: this.deferred.resolve(ex.value); michael@0: } catch (ex if ex instanceof StopIteration) { michael@0: // The generator function terminated with no specific result. michael@0: this.deferred.resolve(); michael@0: } catch (ex) { michael@0: // The generator function failed with an uncaught exception. michael@0: this._handleException(ex); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handle a value yielded by a generator. michael@0: * michael@0: * @param aValue michael@0: * The yielded value to handle. michael@0: */ michael@0: _handleResultValue: function TaskImpl_handleResultValue(aValue) { michael@0: // If our task yielded an iterator resulting from calling another michael@0: // generator function, automatically spawn a task from it, effectively michael@0: // turning it into a promise that is fulfilled on task completion. michael@0: if (isGenerator(aValue)) { michael@0: aValue = Task.spawn(aValue); michael@0: } michael@0: michael@0: if (aValue && typeof(aValue.then) == "function") { michael@0: // We have a promise object now. When fulfilled, call again into this michael@0: // function to continue the task, with either a resolution or rejection michael@0: // condition. michael@0: aValue.then(this._run.bind(this, true), michael@0: this._run.bind(this, false)); michael@0: } else { michael@0: // If our task yielded a value that is not a promise, just continue and michael@0: // pass it directly as the result of the yield statement. michael@0: this._run(true, aValue); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handle an uncaught exception thrown from a generator. michael@0: * michael@0: * @param aException michael@0: * The uncaught exception to handle. michael@0: */ michael@0: _handleException: function TaskImpl_handleException(aException) { michael@0: if (aException && typeof aException == "object" && "name" in aException && michael@0: ERRORS_TO_REPORT.indexOf(aException.name) != -1) { michael@0: michael@0: // We suspect that the exception is a programmer error, so we now michael@0: // display it using dump(). Note that we do not use Cu.reportError as michael@0: // we assume that this is a programming error, so we do not want end michael@0: // users to see it. Also, if the programmer handles errors correctly, michael@0: // they will either treat the error or log them somewhere. michael@0: michael@0: let stack = ("stack" in aException) ? aException.stack : "not available"; michael@0: dump("*************************\n"); michael@0: dump("A coding exception was thrown and uncaught in a Task.\n\n"); michael@0: dump("Full message: " + aException + "\n"); michael@0: dump("Full stack: " + stack + "\n"); michael@0: dump("*************************\n"); michael@0: } michael@0: michael@0: this.deferred.reject(aException); michael@0: } michael@0: };