[gjs] [modules] Add a promise module
- From: Johan Dahlin <johan src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs] [modules] Add a promise module
- Date: Mon, 7 Jun 2010 15:06:22 +0000 (UTC)
commit 35d70724db4438daebb14fcc22cbbc4ba1a243ac
Author: Johan Dahlin <jdahlin litl com>
Date: Mon Jun 7 12:02:19 2010 -0300
[modules] Add a promise module
Promise is a better API for supporting asynchronous code.
https://bugzilla.gnome.org/show_bug.cgi?id=608450
Makefile-modules.am | 5 +-
modules/promise.js | 337 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 340 insertions(+), 2 deletions(-)
---
diff --git a/Makefile-modules.am b/Makefile-modules.am
index 8b177ff..af04ccf 100644
--- a/Makefile-modules.am
+++ b/Makefile-modules.am
@@ -8,8 +8,9 @@ dist_gjsjs_DATA += \
modules/lang.js \
modules/jsUnit.js \
modules/signals.js \
- modules/cairo.js \
- modules/dbus.js
+ modules/cairo.js \
+ modules/dbus.js \
+ modules/promise.js
gjsnative_LTLIBRARIES += console.la debugger.la gi.la langNative.la mainloop.la gettextNative.la dbusNative.la cairoNative.la
diff --git a/modules/promise.js b/modules/promise.js
new file mode 100644
index 0000000..e2d7206
--- /dev/null
+++ b/modules/promise.js
@@ -0,0 +1,337 @@
+/* Copyright (c) 2009-2010 litl, LLC
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+/* Promises represent a value which will be computed in the future,
+ * although it may not be available yet.
+ *
+ * An "async calls" convention is then built on promises, in
+ * async.js. It is generally possible to write code using only the
+ * Async API without explicitly using Promise. It's also possible
+ * (but often more typing) to write code with explicit Promise
+ * manipulation. One or the other may be nicer in a given instance.
+ *
+ * Convention: when an API returns a promise, that promise should be
+ * "kicked off" already (the thread or main loop source should be
+ * started). It should not be necessary to invoke get() on the promise
+ * to get the computation going.
+ *
+ * Convention: it is OK for Promise.get() to invoke the onReturn or
+ * onError synchronously (_before_ the get() returns). This can
+ * occasionally be surprising, but the alternatives are not
+ * good either and the synchronous result can be useful.
+ */
+
+/** default onError handler, makes debugging missing callbacks easier.
+ * It is a bug if this handler gets called.
+ */
+const DEFAULT_ONERROR = function(e) {
+ logError(e, "No onError handler");
+};
+
+/* This is publicly exported from async.js, but is defined here to
+ * avoid a circular dependency.
+ *
+ * The issue is that we want async.js to build on Promise, but as
+ * a nice tweak want Promise.get to be an async call as defined
+ * in async.js
+ */
+let _asyncFunc = function(f) {
+ /* Create a task from an Asynchronous Function, providing 'this' as
+ * the first argument. */
+ f.asyncCall = function() {
+ let params = Array.slice(arguments);
+ let me = params[0];
+ params[0] = f;
+ return _asyncCall.apply(me, params);
+ };
+ return f;
+};
+
+/* This is publicly exported from async.js, but is defined here to
+ * avoid a circular dependency.
+ *
+ * _asyncCall is needed by _asyncFunc above)
+ */
+let _asyncCall = function(f) {
+ let params = Array.slice(arguments, 1); // take off 'f'
+ let promise = new Promise();
+ let onReturn = function(v) { promise.putReturn(v); };
+ let onError = function(e) { promise.putError(e); };
+ params.unshift(onReturn, onError);
+ f.apply(this /* preserve 'this' */, params);
+ return promise;
+};
+
+/** Prototype for a Promise.
+ *
+ * A promise is a value (or error) that may not be available yet.
+ * To obtain this value, we may need to return to the main loop
+ * or run a main loop recursively.
+ *
+ * @constructor
+ */
+function Promise() { this._queue = []; }
+
+Promise.prototype = {
+ get waiting() {
+ return this.hasOwnProperty('_queue');
+ },
+
+ /** Invoke 'onReturn' on the result of this promise, when
+ * it completes. Any error will invoke 'onError' with the thrown
+ * exception. (The get method of a Promise is itself
+ * an Asynchronous Function, see async.js)
+ */
+ get : _asyncFunc(function(onReturn, onError) {
+ if (!this.waiting)
+ throw new Error("get after get (should be cached!)");
+ onError = onError || DEFAULT_ONERROR; // catch bugs
+ /* no value available yet, queue continuations. */
+ this._queue.push({ onReturn: onReturn, onError: onError });
+ }),
+
+ /** Set a normal value of a promise (only callable once), invoking any
+ * queued onReturn continuations as necessary.
+ */
+ putReturn : function(returnValue) {
+ if (!this.waiting)
+ throw new Error("putReturn after put");
+ // mark this promise as no longer 'waiting'
+ let queue = this._queue;
+ delete this._queue;
+ // prevent further queuing
+ this.get = _asyncFunc(function(onReturn, onError) {
+ onReturn(returnValue);
+ });
+ // okay, now invoke queued callbacks.
+ for each (let cb in queue) {
+ try {
+ cb.onReturn(returnValue);
+ } catch (e) {
+ logError(e, "Error in onReturn callback");
+ // but make sure all other callbacks are still invoked.
+ }
+ }
+ },
+ /** Set an error value of a promise (only callable once), invoking any
+ * queued onError continuations as necessary.
+ */
+ putError : function(errorValue) {
+ if (!this.waiting)
+ throw new Error("putError after put");
+ // mark this promise as no longer 'waiting'
+ let queue = this._queue;
+ delete this._queue;
+ // prevent further queuing
+ this.get = _asyncFunc(function(onReturn, onError) {
+ onError(errorValue);
+ });
+ // okay, now invoke queued callbacks.
+ for each (let cb in queue) {
+ try {
+ cb.onError(errorValue);
+ } catch (e) {
+ logError(e, "Error in onError callback");
+ // but make sure all other callbacks are still invoked.
+ }
+ }
+ },
+
+ /** Utility method: fire off the promise, don't wait for a result (but
+ * log any error which occurs).
+ */
+ fireAndForget: function() {
+ this.get(function(){}, DEFAULT_ONERROR);
+ },
+
+ /** Utility method: sets our return (or error) to the result of
+ * another promise. Allows easily chaining promises. If you
+ * putPromisedReturn(undefined) (or with no args) then it is
+ * equivalent to putReturn(undefined), it immediately completes
+ * the promise but with no result value.
+ */
+ putPromisedReturn : function(promiseOfReturn) {
+ let promise = this;
+ if (promiseOfReturn !== undefined) {
+ promiseOfReturn.get(function(v) {
+ promise.putReturn(v);
+ },
+ function(e) {
+ promise.putError(e);
+ });
+ } else {
+ promise.putReturn();
+ }
+ },
+
+ toString: function() {
+ return "[Promise]";
+ }
+};
+
+/** Create a Promise representing the future construction of an object using
+ * the given constructor (first arg) and arguments array. The value of the
+ * promise is the fully-constructed object.
+ *
+ * The constructor must be a special "async constructor" which has
+ * onComplete and onError functions as the first two args.
+ *
+ * @returns a new promise, with newly-constructed object as expected value
+ */
+let fromConstructor = function(constructor, params) {
+ params = params || [];
+ let newobj = {
+ __proto__: constructor.prototype,
+ constructor: constructor
+ };
+
+ let promise = new Promise();
+ // return the constructed object when the constructor completes
+ let onComplete = function() { promise.putReturn(newobj); };
+ let onError = function(e) { promise.putError(e); };
+ params.unshift(onComplete, onError);
+
+ constructor.apply(newobj, params);
+ return promise;
+};
+
+/** Converts a synchronous function into a promise. This is mostly
+ * useful for testing, since the promise is never actually deferred,
+ * of course. In fact the function gets called immediately.
+ * We don't do anything with threads or the main loop here.
+ *
+ * @returns a new promise, with value set to result of invoking function
+ */
+let fromSync = function(f, params) {
+ params = params || [];
+ let promise = new Promise();
+ try {
+ let v = f.apply(this, params);
+ promise.putReturn(v);
+ } catch (e) {
+ promise.putError(e);
+ }
+ return promise;
+};
+
+/** Converts a value to an already-completed promise.
+ * This is useful when you have a synchronous result
+ * already available and want to return it through an
+ * abstract API that returns a Promise.
+ *
+ * @returns a new promise with the given value already set
+ */
+let fromValue = function(v) {
+ let promise = new Promise();
+ promise.putReturn(v);
+ return promise;
+};
+
+let _oneGeneratorStep = function(g, retval, isException, generatorResultPromise) {
+ try {
+ /* get the next asynchronous task to execute from the generator */
+ let promise = (isException) ? g.throw(retval) : g.send(retval);
+ /* execute it, with a continuation which will send the result
+ * back to the generator (whether normal or exception) and
+ * loop (with a tail call). */
+ promise.get(function(v) {
+ _oneGeneratorStep(g, v, false, generatorResultPromise);
+ }, function(e) {
+ _oneGeneratorStep(g, e, true, generatorResultPromise);
+ });
+ } catch (e) {
+ /* before handling the exception, close the generator */
+ try {
+ g.close();
+ } catch (ee) {
+ /* same semantics as javascript: exception thrown in
+ * finally clause overrides any other exception or
+ * return value. */
+ generatorResultPromise.putError(ee);
+ return;
+ }
+ if (e === StopIteration) {
+ /* generator exited without returning a value. */
+ generatorResultPromise.putReturn(); /* done */
+ } else if ('_asyncRetval' in e) {
+ /* generator exited returning a value. */
+ generatorResultPromise.putReturn(e._asyncRetval);
+ } else {
+ /* generator threw an exception explicitly. */
+ generatorResultPromise.putError(e);
+ }
+ }
+};
+
+/** Converts a generator function (with "this" and array of params)
+ * into a promise that promises the error or return value of the
+ * generator function.
+ *
+ * A generator function should contain statements of the form:
+ *
+ * try {
+ * let promiseResult = yield promise;
+ * } catch (promiseError) {
+ * }
+ *
+ * At each such yield, the fromGenerator() driver will take
+ * back control, let the promise complete, then pass back
+ * control giving the result of the promise (or throwing the error
+ * from the promise).
+ *
+ * The power of this is that the generator function can perform a series
+ * of async actions, blocking on each promise in turn, while still having
+ * a nice apparent flow of control. In other words this is a great way
+ * to implement a chain of async steps, where each depends on the previous.
+ *
+ * The generator function can return a value by calling
+ * Promise.putReturnFromGenerator(), which is implemented somewhat hackily
+ * by throwing a special kind of exception. The result is that the
+ * generator ends, and you can Promise.get() the returned value from
+ * the promise that fromGenerator() gave you. If the generator just
+ * falls off the end without calling Promise.putReturnFromGenerator(), the
+ * promise will have an undefined value.
+ *
+ * @returns a new promise whose eventual value depends on the generator
+ */
+
+let fromGenerator = function(g, self, params) {
+ params = params || [];
+ let generatorResultPromise = new Promise();
+
+ try {
+ let generator = g.apply(self, params);
+ _oneGeneratorStep(generator, undefined, false, generatorResultPromise);
+ } catch (e) {
+ /* catch exceptions invoking g() to create generator. */
+ generatorResultPromise.putError(e);
+ }
+
+ return generatorResultPromise;
+};
+
+/** Calls putReturn() on the promise returned by fromGenerator(),
+ * ending that generator and completing its promise.
+ */
+let putReturnFromGenerator = function(val) {
+ throw { _asyncRetval: val };
+};
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]