Skip to content

Commit

Permalink
Rearchitect Q for closure avoidance and WeakMap
Browse files Browse the repository at this point in the history
Implement the `Promise` constructor.  The promise constructor serves
both as a deferred promise constructor that accepts a function, and a
new kind of promise constructor that accepts a backing handler object.
The backing handler object must implement `dispatch(resolve, op,
operands)` and `inspect()`.  The new promise constructor replaces the
`Q.promise` function, which is deprecated.  The new promise constructor
replaces `makePromise`, which has been removed entirely.  As such,
Q-Connection will have to be rearchitected to provide a custom promise
handler for remote objects instead of using `makePromise`. Fixes #346.

Postpone calling `then` on a thenable until a message is dispatched to
the coerced promise.  Fixes #372.

When coercing a thenable, memoize the resulting promise to avoid
re-starting a lazy promise.

Add support for vicious cycle detection.  Fixes #223.

This change request also reviews the Q API, deprecating many interfaces
that remain from legacy designs.  Fixes #215.

Factor most Node.js tools into `q/node` module.  Mirror deprecated
interfaces in Q.

Support for `close` and `closed` has been removed from `Queue`, which
has additional ramifications for Q-Connection.  I intend to use Q-IO
streams in Q-Connection instead of raw queues.

Most of the Q specifications continue to work after these changes, but
with many deprecation warnings.  The specs have been revised to appease
the deprecation warnings.

:warning: However, the specifications for "progress" have all been
disabled pending a closer investigation to decide whether to fix Q or
fix the specs.

The promise protocol no longer supports "set" and "delete" operations.
Function application is a special case of "post", and for support of
"fbind", it is now possible to pass a "thisp" as a final argument.  The
"when" message is now called simply "then".

Support for pre-ECMAScript 5 has been abandoned outright, pending
review.

Removed:

-   Q.set, promise.set
-   Q.delete, promise.delete
-   Q.nearer
-   Q.master

The following methods of `Q` are deprecated in favor of their
equivalents on the `promise` prototype:

-   `progress`, `thenResolve`, `thenReject`, `isPending`, `isFulfilled`,
    `isRejected`, `dispatch`, `get`, `post`, `invoke`, `keys`

Other deprecations:

-   Q.resolve in favor of Q
-   Q.fulfill in favor of Q
-   Q.isPromiseAlike in favor of Q.isThenable
-   Q.when in favor of Q().then
-   Q.fail and promise.fail in favor of promise.catch
-   Q.fin and promise.fin in favor of promise.finally
-   Q.mapply and promise.mapply in favor of promise.post
-   Q.send and promise.send in favor of promise.invoke
-   Q.mcall and promise.mcall in favor of promise.invoke
-   Q.promise in favor of new Q.Promise with a resolver function
-   Q.makePromise in favor of new Q.Promise with a handler object
-   promise.fbind in favor of Q.fbind
-   deferred.makeNodeResolver() in favor of
    require("q/node").makeNodeResolver(deferred.resolve)
-   promise.passByCopy() in favor of Q.passByCopy(promise),
    provisionally

Node.js wrappers that have been moved into their own module have a
deprecated interface in Q proper:

-   `nodeify`, `denodify`, `nfbind`, `nbind`, `npost`, `ninvoke`

But the following experimental aliases are deprecated and do not exist
in `q/node`:

-   `nsend` for `ninvoke`
-   `nmcall` for `ninvoke`
-   `nmapply` for `npost`
  • Loading branch information
kriskowal committed Oct 21, 2013
1 parent faf86fe commit d93f938
Show file tree
Hide file tree
Showing 8 changed files with 1,282 additions and 1,376 deletions.
79 changes: 78 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
## 1.0.0 :warning: BACKWARD INCOMPATIBILITY

- 1.0! :cake:

## 0.10.0 :warning: BACKWARD INCOMPATIBILITY

This is an intermediate release for projects transitioning to 1.0.
This release is version 1.0 but includes many deprecation warnings to
assist in migration to the new interface.

- :warning: As of `1.0`, Q will require ECMAScript 5. Using `es5-shim`,
nor even `es5-sham`, is not sufficient to make legacy engines
compatible because Q requires a WeakMap shim that depends on ES5
Expand All @@ -22,11 +29,81 @@
https://github.com/drses/weak-map. If you are using Q as a
`<script>`, this has been embedded in the release. If you are using
Q in Node.js, the dependency is taken care of by NPM.
- :warning: Withdrew support for SpiderMonkey style generators. Only
ES6 generators are supported.
- :warning: `Q.nextTick` is no longer supported. Please use `asap`
from the `asap` package directly.
- :warning: `valueOf` has been removed. Please use `inspect().value`
instead.
- :warning: Withdrew support for SpiderMonkey style generators.
- :warning: The promise protocol no longer supports "set", "delete",
and "apply" operations. Function application is a special case of
"post" with an undefined method name, and an additional "thisp"
argument for support of "fbind". The "when" message is now called
simply "then". As such, this version of Q is not compatible with
Q-Connection `v0.5`.
- `spread` now accepts an optional `progressed` argument.
- Promises now support vicious cycle detection. If a deferred promise
ultimately depends upon its own resolution, it will be rejected with
the singleton vicious cycle error.

Q now supports a `Promise` constructor with two forms. `new
Promise(callback(resolve, reject))` and `new Promise(promiseHandler)`.
Promise handlers are a new concept and will serve as the basis for
extensibility.

Added:

- `Promise` for constructing promises of all kinds
- `Promise.cast`

Removed. These have migration shims that simply throw errors.

- `Q.set`, `promise.set`
- `Q.delete`, `promise.delete`
- `Q.makePromise` in favor of the new `Promise` constructor and
promise handler.

The following methods of `Q` are deprecated in favor of their
equivalents on the `promise` prototype:

- `progress`, `thenResolve`, `thenReject`, `isPending`, `isFulfilled`,
`isRejected`, `dispatch`, `get`, `post`, `invoke`, `keys`

Other deprecations:

- `Q.master` is no longer needed
- `Q.resolve` in favor of `Q` or `Promise.cast`
- `Q.fulfill` in favor of `Q` or `Promise.cast`
- `Q.isPromiseAlike` in favor of `Q.isThenable`
- `Q.nearer` in favor of `promise.inspect`
- `Q.when` in favor of `Q().then`
- `Q.fail` and `promise.fail` in favor of `promise.catch`
- `Q.fin` and `promise.fin` in favor of `promise.finally`
- `Q.mapply` and `promise.mapply` in favor of `promise.post`
- `Q.send` and `promise.send` in favor of `promise.invoke`
- `Q.mcall` and `promise.mcall` in favor of `promise.invoke`
- `Q.promise` in favor of `new Q.Promise` with a function
- `Q.makePromise` in favor of `new Q.Promise` with a handler object
- `promise.fbind` in favor of `Q.fbind`
- `deferred.makeNodeResolver()` in favor of
`require("q/node").makeNodeResolver(deferred.resolve)`
- `promise.passByCopy()` in favor of `Q.passByCopy(promise)`,
provisionally

Node.js wrappers that have been moved into their own module have a
deprecated interface in Q proper. Notably, `promise.nodeify` has been
retained as the only Node.js convenience method in Q and on the promise
prototype.

- `denodify`, `nfbind`, `nbind`, `npost`, `ninvoke`

But the following experimental aliases are deprecated and do not exist
in `q/node`:

- `nsend` for `ninvoke`
- `nmcall` for `ninvoke`
- `nmapply` for `npost`


## 0.9.7

Expand Down
11 changes: 6 additions & 5 deletions benchmark/compare-with-callbacks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";

var NQ = require("../node");
var Q = require("../q");
var fs = require("fs");

Expand All @@ -18,7 +19,7 @@ suite("A single simple async operation", function () {
});

suite("A fs.readFile", function () {
var denodeified = Q.denodeify(fs.readFile);
var denodeified = NQ.denodeify(fs.readFile);

set("iterations", 1000);
set("delay", 1000);
Expand All @@ -27,17 +28,17 @@ suite("A fs.readFile", function () {
fs.readFile(__filename, done);
});

bench("with Q.nfcall", function (done) {
Q.nfcall(fs.readFile, __filename).then(done);
bench("with NQ.nfcall", function (done) {
NQ.nfcall(fs.readFile, __filename).then(done);
});

bench("with a Q.denodeify'ed version", function (done) {
bench("with a NQ.denodeify'ed version", function (done) {
denodeified(__filename).then(done);
});

bench("with manual usage of deferred.makeNodeResolver", function (done) {
var deferred = Q.defer();
fs.readFile(__filename, deferred.makeNodeResolver());
fs.readFile(__filename, NQ.makeNodeResolver(deferred.resolve));
deferred.promise.then(done);
});
});
Expand Down
135 changes: 135 additions & 0 deletions node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@

var Q = require("./q");
var NQ = exports;

/**
* Creates a Node-style callback that will resolve or reject the deferred
* promise.
* @returns a nodeback
*/
NQ.makeNodeResolver = function (resolve) {
return function (error, value) {
if (error) {
resolve(Q.reject(error));
} else if (arguments.length > 2) {
resolve(Array.prototype.slice.call(arguments, 1));
} else {
resolve(value);
}
};
};

/**
* Passes a continuation to a Node function, which is called with the given
* arguments provided as an array, and returns a promise.
*
* NQ.nfapply(FS.readFile, [__filename])
* .then(function (content) {
* })
*
*/
NQ.nfapply = function (callback, args) {
var deferred = Q.defer();
var nodeArgs = Array.prototype.slice.call(args);
nodeArgs.push(NQ.makeNodeResolver(deferred.resolve));
Q(callback).fapply(nodeArgs).catch(deferred.reject);
return deferred.promise;
};

/**
* Passes a continuation to a Node function, which is called with the given
* arguments provided individually, and returns a promise.
* @example
* Q.nfcall(FS.readFile, __filename)
* .then(function (content) {
* })
*
*/
NQ.nfcall = function (callback /*...args*/) {
var args = Array.prototype.slice.call(arguments, 1);
return NQ.nfapply(callback, args);
};

/**
* Wraps a NodeJS continuation passing function and returns an equivalent
* version that returns a promise.
* @example
* Q.nfbind(FS.readFile, __filename)("utf-8")
* .then(console.log)
* .done()
*/
NQ.nfbind =
NQ.denodeify = function (callback /*...args*/) {
var baseArgs = Array.prototype.slice.call(arguments, 1);
return function () {
var nodeArgs = baseArgs.concat(Array.prototype.slice.call(arguments));
var deferred = Q.defer();
nodeArgs.push(NQ.makeNodeResolver(deferred.resolve));
Q(callback).fapply(nodeArgs).catch(deferred.reject);
return deferred.promise;
};
};

NQ.nbind = function (callback, thisp /*...args*/) {
var baseArgs = Array.prototype.slice.call(arguments, 2);
return function () {
var nodeArgs = baseArgs.concat(Array.prototype.slice.call(arguments));
var deferred = Q.defer();
nodeArgs.push(NQ.makeNodeResolver(deferred.resolve));
function bound() {
return callback.apply(thisp, arguments);
}
Q(bound).fapply(nodeArgs).catch(deferred.reject);
return deferred.promise;
};
};

/**
* Calls a method of a Node-style object that accepts a Node-style
* callback with a given array of arguments, plus a provided callback.
* @param object an object that has the named method
* @param {String} name name of the method of object
* @param {Array} args arguments to pass to the method; the callback
* will be provided by Q and appended to these arguments.
* @returns a promise for the value or error
*/
NQ.npost = function (object, name, nodeArgs) {
var deferred = Q.defer();
nodeArgs.push(NQ.makeNodeResolver(deferred.resolve));
Q(object).dispatch("post", [name, nodeArgs]).catch(deferred.reject);
return deferred.promise;
};

/**
* Calls a method of a Node-style object that accepts a Node-style
* callback, forwarding the given variadic arguments, plus a provided
* callback argument.
* @param object an object that has the named method
* @param {String} name name of the method of object
* @param ...args arguments to pass to the method; the callback will
* be provided by Q and appended to these arguments.
* @returns a promise for the value or error
*/
NQ.ninvoke = function (object, name /*...args*/) {
var nodeArgs = Array.prototype.slice.call(arguments, 2);
var deferred = Q.defer();
nodeArgs.push(NQ.makeNodeResolver(deferred.resolve));
Q(object).dispatch("post", [name, nodeArgs]).catch(deferred.reject);
return deferred.promise;
};

/**
* If a function would like to support both Node continuation-passing-style and
* promise-returning-style, it can end its internal promise chain with
* `nodeify(nodeback)`, forwarding the optional nodeback argument. If the user
* elects to use a nodeback, the result will be sent there. If they do not
* pass a nodeback, they will receive the result promise.
* @param object a result (or a promise for a result)
* @param {Function} nodeback a Node.js-style callback
* @returns either the promise or nothing
*/
NQ.nodeify = nodeify;
function nodeify(object, nodeback) {
return Q(object).nodeify(nodeback);
}

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"weak-map": "~1.0.0"
},
"devDependencies": {
"jshint": "~2.1.9",
"jshint": "~2.3.0",
"cover": "*",
"jasmine-node": "1.11.0",
"opener": "*",
Expand Down
Loading

0 comments on commit d93f938

Please sign in to comment.