Update coroutines to support Promises, cancelation
authorJeremy Maitin-Shepard <jeremy@jeremyms.com>
Wed, 15 Jan 2014 21:06:58 +0000 (15 13:06 -0800)
committerJeremy Maitin-Shepard <jeremy@jeremyms.com>
Wed, 12 Feb 2014 02:40:59 +0000 (11 18:40 -0800)
`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 [new file with mode: 0644]
modules/coroutine.js

diff --git a/modules/compat/Promise.jsm b/modules/compat/Promise.jsm
new file mode 100644 (file)
index 0000000..25c1f75
--- /dev/null
@@ -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:
+ *
+ * <http://promises-aplus.github.com/promises-spec/>
+ *
+ * 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);
+  },
+};
index 0779f83..b246bf7 100644 (file)
@@ -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
  * return `undefined' to the caller. In order to return a value,
  * though, the special syntax:
  *
- * yield co_return(<expr>);
+ *   yield co_return(<expr>);
  *
  * must be used in place of the normal syntax:
  *
- * return <expr>;
+ *   return <expr>;
  *
  * --- Invoking another coroutine function synchronously ---
  *
  * opposed to being invoked asynchronously, in which case it would be
  * run in a new "thread". This is done using the syntax:
  *
- * yield <prepared-coroutine-expr>
+ *   yield <prepared-coroutine-expr>
  *
  * where <prepared-coroutine-expr> 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'.
  * 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'.
  * 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
  * 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 <promise>
+ *
+ * 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
  * 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.
  * 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(<prepared-coroutine-expr>)
+ *   spawn(<prepared-coroutine-expr>)
  *
  * 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;
 }