From 05e21ba3686f9f668f7eaa4635d411fa045308f4 Mon Sep 17 00:00:00 2001 From: Jeremy Maitin-Shepard Date: Wed, 15 Jan 2014 13:06:58 -0800 Subject: [PATCH] Update coroutines to support Promises, cancelation `co_call' and SUSPEND/CONTINUATION are now deprecated, and `spawn' should be used instead for Promise and cancelation support. modules/compat/Promise.jsm is used for Gecko versions older than 25. It is derived from the Firefox 26.0 Promise.jsm, but was changed to be syntax-compatible with Firefox 4.0. --- modules/compat/Promise.jsm | 613 +++++++++++++++++++++++++++++++++++++++++++++ modules/coroutine.js | 496 +++++++++++++++++++++++------------- 2 files changed, 928 insertions(+), 181 deletions(-) create mode 100644 modules/compat/Promise.jsm diff --git a/modules/compat/Promise.jsm b/modules/compat/Promise.jsm new file mode 100644 index 0000000..25c1f75 --- /dev/null +++ b/modules/compat/Promise.jsm @@ -0,0 +1,613 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file was copied from Firefox 26.0. However, it was modified + * slightly to use only syntax compatible with Firefox 4.0. + **/ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "Promise" +]; + +/** + * This module implements the "promise" construct, according to the + * "Promises/A+" proposal as known in April 2013, documented here: + * + * + * + * A promise is an object representing a value that may not be available yet. + * Internally, a promise can be in one of three states: + * + * - Pending, when the final value is not available yet. This is the only state + * that may transition to one of the other two states. + * + * - Resolved, when and if the final value becomes available. A resolution + * value becomes permanently associated with the promise. This may be any + * value, including "undefined". + * + * - Rejected, if an error prevented the final value from being determined. A + * rejection reason becomes permanently associated with the promise. This may + * be any value, including "undefined", though it is generally an Error + * object, like in exception handling. + * + * A reference to an existing promise may be received by different means, for + * example as the return value of a call into an asynchronous API. In this + * case, the state of the promise can be observed but not directly controlled. + * + * To observe the state of a promise, its "then" method must be used. This + * method registers callback functions that are called as soon as the promise is + * either resolved or rejected. The method returns a new promise, that in turn + * is resolved or rejected depending on the state of the original promise and on + * the behavior of the callbacks. For example, unhandled exceptions in the + * callbacks cause the new promise to be rejected, even if the original promise + * is resolved. See the documentation of the "then" method for details. + * + * Promises may also be created using the "Promise.defer" function, the main + * entry point of this module. The function, along with the new promise, + * returns separate methods to change its state to be resolved or rejected. + * See the documentation of the "Deferred" prototype for details. + * + * ----------------------------------------------------------------------------- + * + * Cu.import("resource://gre/modules/Promise.jsm"); + * + * // This function creates and returns a new promise. + * function promiseValueAfterTimeout(aValue, aTimeout) + * { + * let deferred = Promise.defer(); + * + * try { + * // An asynchronous operation will trigger the resolution of the promise. + * // In this example, we don't have a callback that triggers a rejection. + * do_timeout(aTimeout, function () { + * deferred.resolve(aValue); + * }); + * } catch (ex) { + * // Generally, functions returning promises propagate exceptions through + * // the returned promise, though they may also choose to fail early. + * deferred.reject(ex); + * } + * + * // We don't return the deferred to the caller, but only the contained + * // promise, so that the caller cannot accidentally change its state. + * return deferred.promise; + * } + * + * // This code uses the promise returned be the function above. + * let promise = promiseValueAfterTimeout("Value", 1000); + * + * let newPromise = promise.then(function onResolve(aValue) { + * do_print("Resolved with this value: " + aValue); + * }, function onReject(aReason) { + * do_print("Rejected with this reason: " + aReason); + * }); + * + * // Unexpected errors should always be reported at the end of a promise chain. + * newPromise.then(null, Components.utils.reportError); + * + * ----------------------------------------------------------------------------- + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Globals + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/Services.jsm"); + +const STATUS_PENDING = 0; +const STATUS_RESOLVED = 1; +const STATUS_REJECTED = 2; + +// These "private names" allow some properties of the Promise object to be +// accessed only by this module, while still being visible on the object +// manually when using a debugger. They don't strictly guarantee that the +// properties are inaccessible by other code, but provide enough protection to +// avoid using them by mistake. +const salt = Math.floor(Math.random() * 100); +const Name = function (n) { return "{private:" + n + ":" + salt + "}"; } +const N_STATUS = Name("status"); +const N_VALUE = Name("value"); +const N_HANDLERS = Name("handlers"); + +// The following error types are considered programmer errors, which should be +// reported (possibly redundantly) so as to let programmers fix their code. +const ERRORS_TO_REPORT = ["EvalError", "RangeError", "ReferenceError", "TypeError"]; + +//////////////////////////////////////////////////////////////////////////////// +//// Promise + +/** + * This object provides the public module functions. + */ +this.Promise = Object.freeze({ + /** + * Creates a new pending promise and provides methods to resolve or reject it. + * + * @return A new object, containing the new promise in the "promise" property, + * and the methods to change its state in the "resolve" and "reject" + * properties. See the Deferred documentation for details. + */ + defer: function () + { + return new Deferred(); + }, + + /** + * Creates a new promise resolved with the specified value, or propagates the + * state of an existing promise. + * + * @param aValue + * If this value is not a promise, including "undefined", it becomes + * the resolution value of the returned promise. If this value is a + * promise, then the returned promise will eventually assume the same + * state as the provided promise. + * + * @return A promise that can be pending, resolved, or rejected. + */ + resolve: function (aValue) + { + let promise = new PromiseImpl(); + PromiseWalker.completePromise(promise, STATUS_RESOLVED, aValue); + return promise; + }, + + /** + * Creates a new promise rejected with the specified reason. + * + * @param aReason + * The rejection reason for the returned promise. Although the reason + * can be "undefined", it is generally an Error object, like in + * exception handling. + * + * @return A rejected promise. + * + * @note The aReason argument should not be a promise. Using a rejected + * promise for the value of aReason would make the rejection reason + * equal to the rejected promise itself, and not its rejection reason. + */ + reject: function (aReason) + { + let promise = new PromiseImpl(); + PromiseWalker.completePromise(promise, STATUS_REJECTED, aReason); + return promise; + }, + + /** + * Returns a promise that is resolved or rejected when all values are + * resolved or any is rejected. + * + * @param aValues + * Array of promises that may be pending, resolved, or rejected. When + * all are resolved or any is rejected, the returned promise will be + * resolved or rejected as well. + * + * @return A new promise that is fulfilled when all values are resolved or + * that is rejected when any of the values are rejected. Its + * resolution value will be an array of all resolved values in the + * given order, or undefined if aValues is an empty array. The reject + * reason will be forwarded from the first promise in the list of + * given promises to be rejected. + */ + all: function (aValues) + { + if (!Array.isArray(aValues)) { + throw new Error("Promise.all() expects an array of promises or values."); + } + + if (!aValues.length) { + return Promise.resolve([]); + } + + let countdown = aValues.length; + let deferred = Promise.defer(); + let resolutionValues = new Array(countdown); + + function checkForCompletion(aValue, aIndex) { + resolutionValues[aIndex] = aValue; + + if (--countdown === 0) { + deferred.resolve(resolutionValues); + } + } + + for (let i = 0; i < aValues.length; i++) { + let index = i; + let value = aValues[i]; + let resolve = function (val) { return checkForCompletion(val, index); }; + + if (value && typeof(value.then) == "function") { + value.then(resolve, deferred.reject); + } else { + // Given value is not a promise, forward it as a resolution value. + resolve(value); + } + } + + return deferred.promise; + }, +}); + +//////////////////////////////////////////////////////////////////////////////// +//// PromiseWalker + +/** + * This singleton object invokes the handlers registered on resolved and + * rejected promises, ensuring that processing is not recursive and is done in + * the same order as registration occurred on each promise. + * + * There is no guarantee on the order of execution of handlers registered on + * different promises. + */ +this.PromiseWalker = { + /** + * Singleton array of all the unprocessed handlers currently registered on + * resolved or rejected promises. Handlers are removed from the array as soon + * as they are processed. + */ + handlers: [], + + /** + * Called when a promise needs to change state to be resolved or rejected. + * + * @param aPromise + * Promise that needs to change state. If this is already resolved or + * rejected, this method has no effect. + * @param aStatus + * New desired status, either STATUS_RESOLVED or STATUS_REJECTED. + * @param aValue + * Associated resolution value or rejection reason. + */ + completePromise: function (aPromise, aStatus, aValue) + { + // Do nothing if the promise is already resolved or rejected. + if (aPromise[N_STATUS] != STATUS_PENDING) { + return; + } + + // Resolving with another promise will cause this promise to eventually + // assume the state of the provided promise. + if (aStatus == STATUS_RESOLVED && aValue && + typeof(aValue.then) == "function") { + aValue.then(this.completePromise.bind(this, aPromise, STATUS_RESOLVED), + this.completePromise.bind(this, aPromise, STATUS_REJECTED)); + return; + } + + // Change the promise status and schedule our handlers for processing. + aPromise[N_STATUS] = aStatus; + aPromise[N_VALUE] = aValue; + if (aPromise[N_HANDLERS].length > 0) { + this.schedulePromise(aPromise); + } + }, + + /** + * Sets up the PromiseWalker loop to start on the next tick of the event loop + */ + scheduleWalkerLoop: function() + { + this.walkerLoopScheduled = true; + Services.tm.currentThread.dispatch(this.walkerLoop, + Ci.nsIThread.DISPATCH_NORMAL); + }, + + /** + * Schedules the resolution or rejection handlers registered on the provided + * promise for processing. + * + * @param aPromise + * Resolved or rejected promise whose handlers should be processed. It + * is expected that this promise has at least one handler to process. + */ + schedulePromise: function (aPromise) + { + // Migrate the handlers from the provided promise to the global list. + let handlers = aPromise[N_HANDLERS]; + for (let i in handlers) { + this.handlers.push(handlers[i]); + } + aPromise[N_HANDLERS].length = 0; + + // Schedule the walker loop on the next tick of the event loop. + if (!this.walkerLoopScheduled) { + this.scheduleWalkerLoop(); + } + }, + + /** + * Indicates whether the walker loop is currently scheduled for execution on + * the next tick of the event loop. + */ + walkerLoopScheduled: false, + + /** + * Processes all the known handlers during this tick of the event loop. This + * eager processing is done to avoid unnecessarily exiting and re-entering the + * JavaScript context for each handler on a resolved or rejected promise. + * + * This function is called with "this" bound to the PromiseWalker object. + */ + walkerLoop: function () + { + // If there is more than one handler waiting, reschedule the walker loop + // immediately. Otherwise, use walkerLoopScheduled to tell schedulePromise() + // to reschedule the loop if it adds more handlers to the queue. This makes + // this walker resilient to the case where one handler does not return, but + // starts a nested event loop. In that case, the newly scheduled walker will + // take over. In the common case, the newly scheduled walker will be invoked + // after this one has returned, with no actual handler to process. This + // small overhead is required to make nested event loops work correctly, but + // occurs at most once per resolution chain, thus having only a minor + // impact on overall performance. + if (this.handlers.length > 1) { + this.scheduleWalkerLoop(); + } else { + this.walkerLoopScheduled = false; + } + + // Process all the known handlers eagerly. + while (this.handlers.length > 0) { + this.handlers.shift().process(); + } + }, +}; + +// Bind the function to the singleton once. +PromiseWalker.walkerLoop = PromiseWalker.walkerLoop.bind(PromiseWalker); + +//////////////////////////////////////////////////////////////////////////////// +//// Deferred + +/** + * Returned by "Promise.defer" to provide a new promise along with methods to + * change its state. + */ +function Deferred() +{ + this.promise = new PromiseImpl(); + this.resolve = this.resolve.bind(this); + this.reject = this.reject.bind(this); + + Object.freeze(this); +} + +Deferred.prototype = { + /** + * A newly created promise, initially in the pending state. + */ + promise: null, + + /** + * Resolves the associated promise with the specified value, or propagates the + * state of an existing promise. If the associated promise has already been + * resolved or rejected, this method does nothing. + * + * This function is bound to its associated promise when "Promise.defer" is + * called, and can be called with any value of "this". + * + * @param aValue + * If this value is not a promise, including "undefined", it becomes + * the resolution value of the associated promise. If this value is a + * promise, then the associated promise will eventually assume the same + * state as the provided promise. + * + * @note Calling this method with a pending promise as the aValue argument, + * and then calling it again with another value before the promise is + * resolved or rejected, has unspecified behavior and should be avoided. + */ + resolve: function (aValue) { + PromiseWalker.completePromise(this.promise, STATUS_RESOLVED, aValue); + }, + + /** + * Rejects the associated promise with the specified reason. If the promise + * has already been resolved or rejected, this method does nothing. + * + * This function is bound to its associated promise when "Promise.defer" is + * called, and can be called with any value of "this". + * + * @param aReason + * The rejection reason for the associated promise. Although the + * reason can be "undefined", it is generally an Error object, like in + * exception handling. + * + * @note The aReason argument should not generally be a promise. In fact, + * using a rejected promise for the value of aReason would make the + * rejection reason equal to the rejected promise itself, not to the + * rejection reason of the rejected promise. + */ + reject: function (aReason) { + PromiseWalker.completePromise(this.promise, STATUS_REJECTED, aReason); + }, +}; + +//////////////////////////////////////////////////////////////////////////////// +//// PromiseImpl + +/** + * The promise object implementation. This includes the public "then" method, + * as well as private state properties. + */ +function PromiseImpl() +{ + /* + * Internal status of the promise. This can be equal to STATUS_PENDING, + * STATUS_RESOLVED, or STATUS_REJECTED. + */ + Object.defineProperty(this, N_STATUS, { value: STATUS_PENDING, + writable: true }); + + /* + * When the N_STATUS property is STATUS_RESOLVED, this contains the final + * resolution value, that cannot be a promise, because resolving with a + * promise will cause its state to be eventually propagated instead. When the + * N_STATUS property is STATUS_REJECTED, this contains the final rejection + * reason, that could be a promise, even if this is uncommon. + */ + Object.defineProperty(this, N_VALUE, { writable: true }); + + /* + * Array of Handler objects registered by the "then" method, and not processed + * yet. Handlers are removed when the promise is resolved or rejected. + */ + Object.defineProperty(this, N_HANDLERS, { value: [] }); + + Object.seal(this); +} + +PromiseImpl.prototype = { + /** + * Calls one of the provided functions as soon as this promise is either + * resolved or rejected. A new promise is returned, whose state evolves + * depending on this promise and the provided callback functions. + * + * The appropriate callback is always invoked after this method returns, even + * if this promise is already resolved or rejected. You can also call the + * "then" method multiple times on the same promise, and the callbacks will be + * invoked in the same order as they were registered. + * + * @param aOnResolve + * If the promise is resolved, this function is invoked with the + * resolution value of the promise as its only argument, and the + * outcome of the function determines the state of the new promise + * returned by the "then" method. In case this parameter is not a + * function (usually "null"), the new promise returned by the "then" + * method is resolved with the same value as the original promise. + * + * @param aOnReject + * If the promise is rejected, this function is invoked with the + * rejection reason of the promise as its only argument, and the + * outcome of the function determines the state of the new promise + * returned by the "then" method. In case this parameter is not a + * function (usually left "undefined"), the new promise returned by the + * "then" method is rejected with the same reason as the original + * promise. + * + * @return A new promise that is initially pending, then assumes a state that + * depends on the outcome of the invoked callback function: + * - If the callback returns a value that is not a promise, including + * "undefined", the new promise is resolved with this resolution + * value, even if the original promise was rejected. + * - If the callback throws an exception, the new promise is rejected + * with the exception as the rejection reason, even if the original + * promise was resolved. + * - If the callback returns a promise, the new promise will + * eventually assume the same state as the returned promise. + * + * @note If the aOnResolve callback throws an exception, the aOnReject + * callback is not invoked. You can register a rejection callback on + * the returned promise instead, to process any exception occurred in + * either of the callbacks registered on this promise. + */ + then: function (aOnResolve, aOnReject) + { + let handler = new Handler(this, aOnResolve, aOnReject); + this[N_HANDLERS].push(handler); + + // Ensure the handler is scheduled for processing if this promise is already + // resolved or rejected. + if (this[N_STATUS] != STATUS_PENDING) { + PromiseWalker.schedulePromise(this); + } + + return handler.nextPromise; + }, +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Handler + +/** + * Handler registered on a promise by the "then" function. + */ +function Handler(aThisPromise, aOnResolve, aOnReject) +{ + this.thisPromise = aThisPromise; + this.onResolve = aOnResolve; + this.onReject = aOnReject; + this.nextPromise = new PromiseImpl(); +} + +Handler.prototype = { + /** + * Promise on which the "then" method was called. + */ + thisPromise: null, + + /** + * Unmodified resolution handler provided to the "then" method. + */ + onResolve: null, + + /** + * Unmodified rejection handler provided to the "then" method. + */ + onReject: null, + + /** + * New promise that will be returned by the "then" method. + */ + nextPromise: null, + + /** + * Called after thisPromise is resolved or rejected, invokes the appropriate + * callback and propagates the result to nextPromise. + */ + process: function() + { + // The state of this promise is propagated unless a handler is defined. + let nextStatus = this.thisPromise[N_STATUS]; + let nextValue = this.thisPromise[N_VALUE]; + + try { + // If a handler is defined for either resolution or rejection, invoke it + // to determine the state of the next promise, that will be resolved with + // the returned value, that can also be another promise. + if (nextStatus == STATUS_RESOLVED) { + if (typeof(this.onResolve) == "function") { + nextValue = this.onResolve(nextValue); + } + } else if (typeof(this.onReject) == "function") { + nextValue = this.onReject(nextValue); + nextStatus = STATUS_RESOLVED; + } + } catch (ex) { + + // An exception has occurred in the handler. + + if (ex && typeof ex == "object" && "name" in ex && + ERRORS_TO_REPORT.indexOf(ex.name) != -1) { + + // We suspect that the exception is a programmer error, so we now + // display it using dump(). Note that we do not use Cu.reportError as + // we assume that this is a programming error, so we do not want end + // users to see it. Also, if the programmer handles errors correctly, + // they will either treat the error or log them somewhere. + + dump("A coding exception was thrown in a Promise " + + ((nextStatus == STATUS_RESOLVED) ? "resolution":"rejection") + + " callback.\n"); + dump("Full message: " + ex + "\n"); + dump("See https://developer.mozilla.org/Mozilla/JavaScript_code_modules/Promise.jsm/Promise\n"); + dump("Full stack: " + (("stack" in ex)?ex.stack:"not available") + "\n"); + } + + // Additionally, reject the next promise. + nextStatus = STATUS_REJECTED; + nextValue = ex; + } + + // Propagate the newly determined state to the next promise. + PromiseWalker.completePromise(this.nextPromise, nextStatus, nextValue); + }, +}; diff --git a/modules/coroutine.js b/modules/coroutine.js index 0779f83..b246bf7 100644 --- a/modules/coroutine.js +++ b/modules/coroutine.js @@ -1,5 +1,5 @@ /** - * (C) Copyright 2008 Jeremy Maitin-Shepard + * (C) Copyright 2008, 2014 Jeremy Maitin-Shepard * * Use, modification, and distribution are subject to the terms specified in the * COPYING file. @@ -9,6 +9,16 @@ * Coroutine (i.e. cooperative multithreading) implementation in * JavaScript based on Mozilla JavaScript 1.7 generators. * + * This is very similar to the Task mechanism implemented in Task.jsm: + * https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Task.jsm + * + * Like Task.jsm, Conkeror's coroutines integrate with Promises: + * https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm + * + * Conkeror uses resource://gre/modules/Promise.jsm if it is available (Gecko >= + * 25); otherwise, a copy of Gecko 26 Promise.jsm file in + * modules/compat/Promise.jsm is used. + * * Before trying to understand this file, first read about generators * as described here: * https://developer.mozilla.org/en/New_in_JavaScript_1.7 @@ -28,7 +38,7 @@ * generator function `foo' can be invoked using the same syntax as * any other function, i.e.: * - * foo(a,b,c) + * foo(a,b,c) * * this "function call" merely serves to bind the arguments (including * the special `this' argument) without actually running any of the @@ -70,11 +80,11 @@ * return `undefined' to the caller. In order to return a value, * though, the special syntax: * - * yield co_return(); + * yield co_return(); * * must be used in place of the normal syntax: * - * return ; + * return ; * * --- Invoking another coroutine function synchronously --- * @@ -86,17 +96,17 @@ * opposed to being invoked asynchronously, in which case it would be * run in a new "thread". This is done using the syntax: * - * yield + * yield * * where is some expression that evaluates to * a generator object, most typically a direct call to a coroutine * function in the form of * - * yield foo(a,b,c) + * yield foo(a,b,c) * * or * - * yield obj.foo(a,b,c) + * yield obj.foo(a,b,c) * * in the case that "foo" is a coroutine method of some object * `obj'. @@ -104,7 +114,7 @@ * If the specified coroutine returns a value normally, the yield * expression evaluates to that value. That is, using the the syntax * - * var x = yield foo(a,b,c); + * var x = yield foo(a,b,c); * * if foo is a coroutine and returns a normal value, that value will * be stored in `x'. @@ -118,7 +128,7 @@ * Note that it is safe to invoke a normal function using `yield' as * well as if it were a coroutine. That is, the syntax * - * yield foo(a,b,c) + * yield foo(a,b,c) * * can likewise be used if foo is a normal function, and the same * return value and exception propagation semantics @@ -132,11 +142,32 @@ * due to the normal exception propagation, yield is never even * called.) * - * --- Current continutation/"thread" handle --- + * --- Integration with Promise API --- + * + * Promises provide a simple, standard interface for asynchronous operations. + * Coroutines can wait synchronously for the result of a Promise by using yield: + * + * yield + * + * Promises are detected by presence of a `then' member of type function. If + * the Promise is resolved, the yield expression will evaluate to the resolved + * value. Otherwise, if the Promise is rejected, the yield expression will + * cause the rejection exception to be thrown. + * + * In effect, a function that starts an asyncronous operation and returns a + * Promise can be called synchronously from a coroutine, in the same way that + * another coroutine can be called synchronously, by using: + * + * yield function_that_returns_a_promise() + * + * --- [[deprecated]] Current continutation/"thread" handle --- + * + * Note: This API is deprecated because it is error-prone. Instead, use the + * Promise integration. * * The special syntax * - * var cc = yield CONTINUATION; + * var cc = yield CONTINUATION; * * can be used to obtain a special "continuation" object that serves * as a sort of "handle" to the current thread. Note that while in a @@ -146,24 +177,24 @@ * The continuation object is used in conjuction with another special * operation: * - * yield SUSPEND; + * yield SUSPEND; * * This operation suspends execution of the current "thread" until it * is resumed using a reference to the continuation object for the * "thread". There are two ways to resume executation. To resume * execution normally, the syntax: * - * cc(value) + * cc(value) * * or * - * cc() (equivalent to cc(undefined)) + * cc() (equivalent to cc(undefined)) * * can be used. This resumes execution and causes the yield SUSPEND * expression to evaluate to the specified value. Alternatively, the * syntax: * - * cc.throw(e) + * cc.throw(e) * * can be used. This causes the specified exception `e' to be thrown * from the yield SUSPEND expression. @@ -189,31 +220,68 @@ * A coroutine function can be called asynchronously from either a * normal function or a coroutine function. Conceptually, this is * equivalent to spawning a new "thread" to run the specified - * coroutine function. This operation is done using the co_call + * coroutine function. This operation is done using the spawn * function as follows: * - * co_call() + * spawn() * * or, for example, * - * co_call(foo(a,b,c)) + * spawn(foo(a,b,c)) * * or * - * co_call(function (){ yield foo(a,b,c); }()) + * spawn(function (){ yield foo(a,b,c); }()) + * + * The `spawn' function returns a Promise representing the result of the + * asyncronous call. * - * In the last example, by modifying the anonymous coroutine function, - * the return value of the coroutine foo, or an exception it throws, - * could be processed, which is not possible in the case that foo is - * called directly by co_call. + * As a convenience, if the argument to `spawn' is a Promise instead of a + * prepared coroutine (i.e. a generator), it is returned as is, such that + * spawn can be used transparently with both generator functions and functions + * returning Promises. * - * The function co_call returns the continuation for the new "thread" - * created. The return value of the specified continuation is ignored, - * as are any exceptions it throws. The call to co_call returns as - * soon as the specified coroutine suspends execution for the first - * time, or completes. + * === Cancelation === + * + * Conkeror's coroutines support an extension to the Promise API for + * cancelation: a Promise may have a `cancel' member of type function, which + * attempts to cancel the corresponding asynchronous operation. + * + * The `cancel' function should not make use of the implicit `this' argument. + * The `cancel' function takes one optional argument (defaults to + * task_canceled()), which specifies the exception with which the Promise should + * be rejected if the cancelation is successful. + * + * The `cancel' function is asynchronous and returns without providing any + * indication of whether the cancelation was successful. There is no guarantee + * that cancelation will be possible or even successful, for instance the + * Promise may have already been resolved or rejected, or the asynchronous + * operation may not be in a cancelable state, or the cancelation notification + * may be ignored for some reason. However, if the cancelation is successful, + * this should be indicated by rejecting the Promise with the specified + * exception. + * + * The helper functions `make_cancelable' and `make_simple_cancelable' are + * useful for creating Promises that support the cancelation API. + * + * The Promise returned by `spawn' supports cancelation, and works as follows: + * + * The `cancel' function in the returned Promise marks the "thread" for + * cancelation. While a "thread" is marked for cancelation, any Promise the + * thread waits on synchronously (including one in progress when `cancel' is + * called) will be immediately canceled if it supports cancelation. The + * "thread" will remain marked for cancelation until one of the cancelation + * requests is successful, i.e. one of the canceled Promises is rejected with + * the cancelation exception. **/ +try { + Components.utils.import("resource://gre/modules/Promise.jsm"); +} catch (e) { + // Gecko < 25 + Components.utils.import("chrome://conkeror/content/compat/Promise.jsm"); +} + function _return_value (x) { this.value = x; } @@ -238,196 +306,262 @@ function is_coroutine (obj) { typeof(obj.send) == "function"; } -function _do_call (f) { +/** + * Returns an object that behaves like a Promise but also has a + * `cancel` method, which invokes the specified `canceler` function. + * The returned object has the specified promise as its prototype. + * Note that we have to create a new object to add the `cancel` method + * because Promise objects are sealed. + * + * The canceler function must take one argument `e`, a cancelation + * exception. The canceler function should arrange so that the + * promise is rejected with `e` if the cancelation is successful. Any + * other result of the promise indicates that the cancelation was not + * successfully delivered. If `e` is undefined, it defaults to + * task_canceled(). + * + * This protocol is important for coroutines as it makes it possible + * to retry delivering the cancelation notification until it is + * delivered successfully at least once. + **/ +function make_cancelable (promise, canceler) { + return { __proto__: promise, + cancel: function (e) { + if (e === undefined) + e = task_canceled(); + canceler(e); + } + }; +} + +/** + * Returns a Promise that supports cancelation by simply rejecting the specified + * `deferred` object with the cancelation exception. + * + * This will likely leave the asynchronous operation running, and a proper + * cancelation function that actually stops the asynchronous operation should be + * used instead when possible. + **/ +function make_simple_cancelable(deferred) { + return make_cancelable(deferred.promise, deferred.reject); +} - /* Suspend immediately so that co_call can pass us the continuation object. */ - var cc = yield undefined; +function task_canceled () { + let e = new Error("task_canceled"); + e.__proto__ = task_canceled.prototype; + return e; +} +task_canceled.prototype.__proto__ = Error.prototype; +function _co_impl (f) { + + // Current generator function currently at top of call stack + this.f = f; /** * Stack of (partially-run) prepared coroutines/generator objects * that specifies the call-chain of the current coroutine function * `f'. Conceptually, `f' is at the top of the stack. **/ - var stack = []; + this.stack = []; /** - * `y' and `throw_value' together specify how execution of `f' will be resumed. - * If `throw_value' is false, f.send(y) is called to resume execution normally. - * If `throw_value' is true, f.throw(y) is called to throw the exception `y' in `f'. - * - * Because `f' is initially a just-created prepared coroutine that has not run any - * code yet, we must start it by calling f.send(undefined) (which is equivalent to - * calling f.next()). + * Deferred object used to return the result of the coroutine. **/ - var y = undefined; - var throw_value = false; - while (true) { - try { // We must capture any exception thrown by `f' - - /** - * If `f' yields again after being resumed, the value - * passed to yield will be stored in `x'. - **/ - let x; - - if (throw_value) { - throw_value = false; - x = f.throw(y); // f.throw returns the next value passed to yield - } else - x = f.send(y); // f.send also returns the next value passed to yield - - if (x === CONTINUATION) { - /** - * The coroutine (`f') asked us to pass it a reference - * to the current continuation. We don't need to make - * any adjustments to the call stack. - **/ - y = cc; - continue; - } + this.deferred = Promise.defer(); + + /** + * The current canceler function, used to interrupt this coroutine. If null, then any cancelation will be delayed. + **/ + this.canceler = null; + + /** + * A pending cancelation. + **/ + this.pending_cancelation = undefined; +} + +_co_impl.prototype = { + constructor: _co_impl, + cancel: function _co_impl__cancel (e) { + this.pending_cancelation = e; + if (this.canceler) { + this.canceler(e); + } + }, + send: function _co_impl__send(throw_value, y) { + if (this.canceler === undefined) { + let e = new Error("Programming error: _co_impl.send called on already-running coroutine."); + dump_error(e); + throw e; + } + this.canceler = undefined; + + // Cancelation has been successfully delivered, remove pending cancelation + if (throw_value && this.pending_cancelation == y && y !== undefined) + this.pending_cancelation = undefined; + + while (true) { + try { // We must capture any exception thrown by `f' - if (x === SUSPEND) { /** - * The coroutine `f' asked us to suspend execution of - * the current thread. We do this by calling yield ourself. + * If `f' yields again after being resumed, the value + * passed to yield will be stored in `x'. **/ - try { - /* our execution will be suspended until send or throw is called on our generator object */ - x = yield undefined; + let x; + if (throw_value) { + throw_value = false; + x = this.f.throw(y); // f.throw returns the next value passed to yield + } else + x = this.f.send(y); // f.send also returns the next value passed to yield + // [[Deprecated]] + // The promise API should be used instead. + if (x === CONTINUATION) { /** - * Since no exception was thrown, user must have requested that we resume - * normally using cc(value); we simply pass that value back to `f', which - * asked us to suspend in the first place. + * The coroutine (`f') asked us to pass it a reference + * to the current continuation. We don't need to make + * any adjustments to the call stack. **/ - y = x; - } catch (e) { - /** - * User requested that we resume by throwing an exception, so we re-throw - * the exception in `f'. - **/ - throw_value = true; - y = e; + let cc = this.send.bind(this, false); + cc.throw = this.send.bind(this, true); + + y = cc; + continue; } - continue; - } - if (is_coroutine(x)) { - // `f' wants to synchronously call the coroutine `x' - stack[stack.length] = f; // push the current coroutine `f' onto the call stack - f = x; // make `x' the new top of the stack - y = undefined; // `x` is a new coroutine so we must start it by passing `undefined' - continue; - } + // [[Deprecated]] + // The promise API should be used instead. + if (x === SUSPEND) { + this.canceler = null; + return; + } + + if (is_coroutine(x)) { + // `f' wants to synchronously call the coroutine `x' + this.stack[this.stack.length] = this.f; // push the current coroutine `f' onto the call stack + this.f = x; // make `x' the new top of the stack + y = undefined; // `x` is a new coroutine so we must start it by passing `undefined' + continue; + } - if (x instanceof _return_value) { - // `f' wants to return a value - f.close(); - if (stack.length == 0) { + if (x && typeof(x.then) == "function") { + // x is assumed to be a Promise + // Wait for result before returning to caller + if (typeof(x.cancel) == "function") { + if (this.pending_cancelation !== undefined) + x.cancel(this.pending_cancelation); + this.canceler = x.cancel; + } else + this.canceler = null; + + x.then(this.send.bind(this, false), this.send.bind(this, true)); + return; + } + + if (x instanceof _return_value) { + // `f' wants to return a value + this.f.close(); + if (this.stack.length == 0) { + /** + * `f' doesn't have a caller, so we resolve + * this.deferred with the return value and + * terminate the coroutine. + */ + this.deferred.resolve(x.value); + return; + } + // Pop the caller of `f' off the top of the stack + this.f = this.stack[this.stack.length - 1]; + this.stack.length--; + // Pass the return value to the caller, which is now the current coroutine + y = x.value; + continue; + } + + /** + * `f' yielded to us a value without any special + * interpretation. Most likely, this is due to `f' calling + * a normal function as if it were a coroutine, in which + * case `x' simply contains the return value of that + * normal function. Just return the value back to `f'. + **/ + y = x; + } catch (e) { + /** + * `f' threw an exception. If `e' is a StopIteration + * exception, then `f' exited without returning a value + * (equivalent to returning a value of + * `undefined'). Otherwise, `f' threw or propagted a real + * exception. + **/ + if (this.stack.length == 0) { /** - * `f' doesn't have a caller, so we simply ignore - * the return value and terminate the thread. + * `f' doesn't have a caller, so we resolve/reject + * this.deferred and terminate the coroutine. */ + if (e instanceof StopIteration) + this.deferred.resolve(undefined); + else + this.deferred.reject(e); return; } // Pop the caller of `f' off the top of the stack - f = stack[stack.length - 1]; - stack.length--; - // Pass the return value to the caller, which is now the current coroutine - y = x.value; - continue; + this.f = this.stack[this.stack.length - 1]; + this.stack.length--; + if (e instanceof StopIteration) + y = undefined; // propagate a return value of `undefined' to the caller + else { + // propagate the exception to the caller + y = e; + throw_value = true; + } } + } + }, +}; - /** - * `f' yielded to us a value without any special - * interpretation. Most likely, this is due to `f' calling - * a normal function as if it were a coroutine, in which - * case `x' simply contains the return value of that - * normal function. Just return the value back to `f'. - **/ - y = x; - } catch (e) { - /** - * `f' threw an exception. If `e' is a StopIteration - * exception, then `f' exited without returning a value - * (equivalent to returning a value of - * `undefined'). Otherwise, `f' threw or propagted a real - * exception. - **/ - if (stack.length == 0) { - /** - * `f' doesn't have a caller, so regardless of whether - * `f' exited normally or threw an exception, we - * simply terminate the thread. - */ - return; - } - // Pop the caller of `f' off the top of the stack - f = stack[stack.length - 1]; - stack.length--; - if (e instanceof StopIteration) - y = undefined; // propagate a return value of `undefined' to the caller - else { - // propagate the exception to the caller - y = e; - throw_value = true; - } +/** + * Runs the specified coroutine asynchronously. Returns a potentially-cancelable + * Promise representing the result. + * + * In the normal case, the `f` is a prepared coroutine (i.e. generator). + * + * If `f` is instead a Promise, it is returned as is, with a no-op canceler. + * + * If `f` is some other value, this returns `Promise.resolve(f)` with a no-op canceler. + **/ +function spawn (f) { + if (!is_coroutine(f)) { + if (f && typeof(f.then) == "function") { + // f is a Promise, just return as is + if (typeof(f.cancel) != "function") + return make_cancelable(f, function () {} ); + return f; } + return make_cancelable(Promise.resolve(f), function () {}); } + + let x = new _co_impl(f); + x.send(false, undefined); // initialize + + return make_cancelable(x.deferred.promise, x.cancel.bind(x)); } /** - * Invokes the specified coroutine. If the argument is not a (already - * prepared) coroutine, this function simply returns, doing - * nothing. Thus, the syntax: - * - * co_call(foo(a,b,c)) - * - * can be used to call the function foo with the arguments [a,b,c] - * regardless of whether foo is a normal function or a coroutine - * function. Note, though, that using this syntax, if foo is a normal - * function and throws an exception, it will be propagated, while if - * foo is a coroutine, any exceptions it throws will be ignored. + * [[deprecated]] Runs the specified coroutine asynchronously. + * Returns the corresponding continuation object. * + * Use `spawn' instead, which returns a Promise rather than a continuation object. **/ function co_call (f) { if (!is_coroutine(f)) return; - /** The _do_call helper function is called to actually do all of - * the work. `_do_call' is written as a generator function for - * implementation convenience. - **/ - var g = _do_call(f); - g.next(); - var cc = function (x) { - try { - // We resume execution of the thread by calling send on - // the generator object corresponding to our invocation of - // _do_call. - g.send(x); - } catch (e if e instanceof StopIteration) {} - catch (e) { - // Dump this error, because it indicates a programming error - dump_error(e); - } - }; - cc.throw = function (x) { - try { - g.throw(x); - } catch (e if e instanceof StopIteration) {} - catch (e) { - // Dump this error, because it indicates a programming error - dump_error(e); - } - }; - try { - g.send(cc); - } catch (e if e instanceof StopIteration) {} - catch (e) { - // Dump this error, because it indicates a programming error - dump_error(e); - } + let x = new _co_impl(f); + x.send(false, undefined); // initialize + + let cc = x.send.bind(this, false); + cc.throw = x.send.bind(this, true); return cc; } -- 2.11.4.GIT