Permalink
Browse files

Commit of big refactor to using generic StepSequence instances and us…

…ing breakTo semantics (replaces the if semantics attached to what are now RouteTriggeredSequence instances)
  • Loading branch information...
1 parent 33c6492 commit 1fded467d443702c4e99c643477c11a79f08f797 @bnoguchi committed May 2, 2011
Showing with 444 additions and 458 deletions.
  1. +22 −3 example/server.js
  2. +2 −0 example/views/register.jade
  3. +0 −190 lib/deprecated.js
  4. +102 −105 lib/everymodule.js
  5. +1 −5 lib/facebook.js
  6. +35 −32 lib/oauth2.js
  7. +33 −3 lib/password.js
  8. +25 −1 lib/promise.js
  9. +17 −0 lib/routeTriggeredSequence.js
  10. +138 −0 lib/step.js
  11. +69 −119 lib/{sequence.js → stepSequence.js}
View
25 example/server.js
@@ -53,9 +53,28 @@ everyauth
.getRegisterPath('/register')
.postRegisterPath('/register')
.registerView('register.jade')
- .registerUser( function (login, password) {
- return usersByLogin[login] = {
- login: login, password: password };
+ .validateRegistration( function (login, password, extraParams, req, res) {
+ if (!login)
+ return this.breakTo('registrationError', req, res, 'Missing login');
+ if (!password)
+ return this.breakTo('registrationError', req, res, 'Missing password');
+
+ // simulate an async user db
+ var promise = this.Promise();
+ setTimeout( function () {
+ if (login in usersByLogin) {
+ return promise.breakTo('registrationError', req, res, 'Someone already has the login ' + login);
+ }
+ promise.fulfill({
+ login: login
+ , password: password
+ });
+ }, 200);
+ return promise;
+ })
+ .registerUser( function (newUserAttrs) {
+ var login = newUserAttrs.login;
+ return usersByLogin[login] = newUserAttrs;
})
.redirectPath('/');
View
2 example/views/register.jade
@@ -1,4 +1,6 @@
h2 Register
+- if ('undefined' !== typeof errorMessage)
+ #error= errorMessage
form(action: '/register', method: 'POST')
#login
label(for: 'login') Login
View
190 lib/deprecated.js
@@ -1,190 +0,0 @@
-/** Facebook **/
-// Introspection
-console.log( fb.steps );
-
-// Make order of steps explicit
-// fb.seq(...)
-fb.steps.order('authRequest', 'authCallback', 'addToSession');
-
-fb.step('authRequest').steps.order(
- 'handleRequest', 'determineScope',
- 'generateAuthUri', 'redirectToAuthUri');
-// `order(...)` should throw an error if it is missing a step
-
-
-
-
-
-
-
-
-
-
-////////////////////////////////////////
-
-function findUser (cred, fbUserMetadata) {
- var p = new Promise();
- User.find({id: 1}, function (err, user) {
- p.succeed(err, user);
- });
- return p;
-}
-
-function orCreateUser(err, user) {
- var p = new Promise();
- if (user) {
- p.succeed(null, user);
- } else {
- User.create({}, function (err, user) {
- p.succeed(err, user);
- });
- }
- return p;
-}
-
-function assignToSession (sess, user, cred, fbData) {
- var p = new Promise();
- // Logic goes here
- p.succeed(sess, user, cred, fbData);
- return p;
-}
-
-function anon (req, res, uid, cred, info) {}
-
-function anon2 (req, res, provider, uid, cred, info) {}
-
-everyauth(
- function (req, res, provider, cred, info) {
- }
- , facebook(
- function (req, res, uid, cred, info) {
- }
- , userLogic(
- )
- )
- , twitter(
- function (req, res, uid, cred, info) {
- }
- )
-);
-
-var fb = module.exports =
-oauthModule.submodule('facebook')
- // TODO submodule should
- // set fb.name = 'facebook'
- .apiHost('https://graph.facebook.com')
- .setters('scope')
- .routeStep('authRequest')
- .uri('/auth/facebook')
- .step('authRequest')
- .accepts('req res abc')
- .returns('*')
- .get('/auth/facebook')
- // Should be able to access module
- // properties from within a step
- // definition
- .step('authCallback')
- .get('/auth/facebook/callback')
- .step('addToSession')
- .define( function (sess, auth) {
-
- })
-
-/** OAuth **/
-
-var oauth = module.exports =
-everyModule.submodule('oauth')
- .setters('apiHost entryUri callbackUri')
- .setupRoutes( function (mod, app) {
- app.get(mod._entryUri, mod.start('/auth/facebook'));
- // mod.start() creates a new instantiation of the promise chain
- // and returns the `trigger` of the first step
- app.get(mod._callbackUri, mod.start('auth/facebook/callback'));
- })
- // Define a sequence of steps named 'auth/facebook'
- .sequence('/auth/facebook')
- .step('requestAuthUri', function (substep) {
- substep('determineScope')
- .accepts('req res')
- .promises('scope')
- .define(fn)
- .error(fn);
- substep('generateAuthUri')
- .accepts('scope')
- .promises('authUri');
- substep('requestAuthUri')
- .accepts('authUri')
- .promises(null);
- })
- // reset vs bridge
- // bridge should create a hook for
- // instantiating a new chain of steps
- .sequence('/auth/facebook/callback')
- .step('retrieveCode')
- .accepts('req res')
- .promises('code')
- .step('retrieveAccessToken')
- .accepts('code')
- .promises('accessToken refreshToken')
- .step('getAuth')
- .accepts('accessToken refreshToken')
- .promises('auth');
- // define can define the step function
- // OR it can be the entry point for defining
- // the sub steps it is composed of
- .define( function (authRequest) {
- authRequest
- })
- .step('authCallback')
- .accepts('req res auth')
- .promises(null);
-
-oauth.step('authRequest/determineScope').define( function (req, res) {
- var scope = this._scope;
- if ('function' === typeof scope) {
- return scope(req);
- }
- return scope;
-});
-
-oauth.step('authRequest/generateAuthUri').define( function (scope) {
- var oauth = this._oauth
- , authUri = oauth.getAuthorizeUrl({
- redirect_uri: this._myHostname + this._callbackUri
- , scope: scope});
- return authUri;
-});
-
-oauth.step('authRequest/retrieveCode').define( function (authUri) {
- var res = this.cache.res;
- res.writeHead(303, {'Location': authUri});
- res.end();
-});
-
-
-// How to add steps to a module or to a composed step
-
-// Adds a series of steps
-oauth.step('authCallback', function (step) {
- step('addCronJob');
-});
-
-// Adds a single step
-oauth.step('authCallback');
-
-// Over-rides a sequence of substeps
-// composedOf vs override
-oauth.step('authCallback').override(function (step) {
- step(...);
-});
-
-oauth.step('authCallback').order('...');
-
-oauth.step('authCallback').addStep('addCronJob');
-
-oauth.step('authCallback').steps.order(...);
-
-oauth.step('authCallback').steps.add('addCronJob', {before: ''});
-
-oauth.step('authCallback').steps.add('addCronJob', {after: ''});
-
View
207 lib/everymodule.js
@@ -1,6 +1,9 @@
var url = require('url')
- , MaterializedSequence = require('./sequence')
- , clone = require('./utils').clone;
+ , Step = require('./step')
+ , StepSequence = require('./stepSequence')
+ , RouteTriggeredSequence = require('./routeTriggeredSequence')
+ , clone = require('./utils').clone
+ , Promise = require('./promise');
var routeDescPrefix = {
get: 'ROUTE (GET)'
@@ -11,21 +14,12 @@ routeDescPrefix.POST = routeDescPrefix.post;
function route (method) {
return function (alias, description) {
- // Clear state
- this._currentCondition = null;
-
- var conds2seqs = this._routes[method][alias] = {}; // maps conditions to sequences
- Object.defineProperty(conds2seqs, 'conditions', {
- value: ['defaultCondition'] // list of condition aliases
- , enumerable: false
- , writable: true
- });
- this.configurable('defaultCondition', 'do not alter this');
-
if (description)
description = routeDescPrefix[method] + ' - ' + description;
this.configurable(alias, description);
- this._currentRoute = [method, alias];
+ var name = method + ':' + alias;
+ this._currSeq =
+ this._stepSequences[name] || (this._stepSequences[name] = new RouteTriggeredSequence(name, this));
return this;
};
}
@@ -53,18 +47,10 @@ var everyModule = module.exports = {
}
, get: route('get')
, post: route('post')
- , if: function (conditionAlias) {
- this._currentCondition = conditionAlias;
-
- var currRoute = this._currentRoute;
- var currAlias = this._routes[currRoute[0]][currRoute[1]];
- var conditions = currAlias.conditions;
-
- // Just in case we are re-opening a condition
- if (~conditions.indexOf(conditionAlias)) return this;
-
- conditions.splice(1, 0, conditionAlias);
- this.configurable(conditionAlias);
+ , stepseq: function (name, description) {
+ this.configurable(name, description);
+ this._currSeq =
+ this._stepSequences[name] || (this._stepSequences[name] = new StepSequence(name, this));
return this;
}
, configurable: function (arg, description, wrapper) {
@@ -118,21 +104,18 @@ var everyModule = module.exports = {
}
return this;
}
+
, step: function (name) {
var steps = this._steps
- , routes = this._routes
- , currRoute = this._currentRoute
- , condition = this._currentCondition || 'defaultCondition';
+ , sequence = this._currSeq;
- var sequence =
- routes[currRoute[0]][currRoute[1]][condition] ||
- // Lazy instantiation (vs instantiation in this.get(...) or this.post(...)
- (routes[currRoute[0]][currRoute[1]][condition] = new MaterializedSequence(this));
-
- if (!currRoute)
+ if (!sequence)
throw new Error("You can only declare a step after declaring a route alias via `get(...)` or `post(...)`.");
+
sequence.orderedStepNames.push(name);
- if (!steps[name]) steps[name] = {name: name};
+
+ this._currentStep =
+ steps[name] || (steps[name] = new Step(name, this));
// For configuring what the actual business
// logic is:
@@ -143,38 +126,50 @@ var everyModule = module.exports = {
// } );
this.configurable(name,
'STEP FN [' + name + '] function encapsulating the logic for the step `' + name + '`.');
- this._currentStep = steps[name];
return this;
}
+
, accepts: function (input) {
var step = this._currentStep;
step.accepts = input
? input.split(/\s+/)
: null;
return this;
}
+
, promises: function (output) {
var step = this._currentStep;
step.promises = output
? output.split(/\s+/)
: null;
return this;
}
+
, description: function (desc) {
var step = this._currentStep;
step.description = desc;
+
if (desc)
desc = 'STEP FN [' + step.name + '] - ' + desc;
this.configurable(step.name, desc);
return this;
}
+
, stepTimeout: function (millis) {
var step = this._currentStep;
step.timeout = millis;
return this;
}
- , cloneOnSubmodule: ['cloneOnSubmodule', '_steps', '_configurable']
+ , canBreakTo: function (sequenceName) {
+ // TODO Implement this (like static typing)
+ // unless `canBreakTo` only needed for
+ // readability
+ return this;
+ }
+
+
+ , cloneOnSubmodule: ['cloneOnSubmodule', '_configurable']
, submodules: {}
@@ -198,32 +193,26 @@ var everyModule = module.exports = {
submodule[toClone] = clone(self[toClone]);
}
);
- submodule._routes = {post: {}, get: {}};
- for (var method in this._routes) {
- for (var alias in this._routes[method]) {
- submodule._routes[method][alias] = {};
- for (var condition in this._routes[method][alias]) {
- var seq = this._routes[method][alias][condition].clone(submodule);
- submodule._routes[method][alias][condition] = seq;
- Object.defineProperty(submodule._routes[method][alias], 'conditions', {
- value: clone(this._routes[method][alias].conditions)
- , enumerable: false
- , writable: true
- });
- }
- }
+
+ var seqs = this._stepSequences
+ , newSeqs = submodule._stepSequences = {};
+ for (var seqName in seqs) {
+ newSeqs[seqName] = seqs[seqName].clone(submodule);
}
+
+ var steps = this._steps
+ , newSteps = submodule._steps = {};
+ for (var stepName in steps) {
+ newSteps[stepName] = steps[stepName].clone(stepName, submodule);
+ }
+
submodule.name = name;
return submodule;
}
, validateSteps: function () {
- for (var method in this._routes) {
- for (var routeAlias in this._routes[method]) {
- for (var conditionAlias in this._routes[method][routeAlias]) {
- this._routes[method][routeAlias][conditionAlias].checkSteps();
- }
- }
+ for (var seqName in this._stepSequences) {
+ this._stepSequences[seqName].checkSteps();
}
}
@@ -233,62 +222,80 @@ var everyModule = module.exports = {
*/
, routeApp: function (app) {
if (this.init) this.init();
- for (var method in this._routes) {
- for (var routeAlias in this._routes[method]) {
- var path = this[routeAlias]();
+ var self = this
+ , routes = this._routes
+ , methods = ['get', 'post'];
+ for (var method in routes) {
+ for (var routeAlias in routes[method]) {
+ var path = self[routeAlias]();
if (!path)
throw new Error('You have not defined a path for the route alias ' + routeAlias + '.');
- app[method](path, this.routeHandler(method, routeAlias));
+ var seq = routes[method][routeAlias];
+
+ // This kicks off a sequence of steps based on a
+ // route
+ // Creates a new chain of promises and exposes the leading promise
+ // to the incoming (req, res) pair from the route handler
+ app[method](path, seq.routeHandler.bind(seq));
}
}
}
- // Returns the route handler
- // This is also where a lot of the magic happens (See ./sequence.js)
- , routeHandler: function (method, routeAlias) {
- var route = this._routes[method][routeAlias]
- , conditions = route.conditions
- , self = this;
-
- return function (req, res) {
- var condition, conditionLambda;
- var i = conditions.length;
- while (i--) {
- condition = conditions[i];
- conditionLambda = self[condition]();
- if (conditionLambda.call(self, req, res))
- break;
- }
- var seq = route[condition];
- if (!seq) {
- throw new Error("None of your conditions passed for route " + method.toUpperCase() + ' ' + routeAlias);
- }
- seq.routeHandler.call(seq, req, res);
- };
+ , Promise: function (values) {
+ return new Promise(this, values);
+ }
- return seq.routeHandler();
- // This kicks off a sequence of steps based on a
- // route
- // Creates a new chain of promises and exposes the leading promise
- // to the incoming (req, res) pair from the route handler
+ /**
+ * breakTo(sequenceName, arg1, arg2, ...);
+ * [arg1, arg2, ...] are the arguments passed to
+ * the `sequence.start(...)` where sequence is the
+ * sequence with name `sequenceName`
+ * @param {String} sequenceName
+ */
+ , breakTo: function (sequenceName) {
+ // TODO Garbage collect the abandoned sequence
+ var seq = this._stepSequences[sequenceName]
+ , args = Array.prototype.slice.call(arguments, 1);
+ if (!seq) {
+ throw new Error('You are trying to break to a sequence named `' + sequenceName + '`, but there is no sequence with that name in the auth module, `' + this.name + '`.');
+ }
+ seq = seq.materialize();
+ seq.initialArgs = args;
+ throw seq;
}
// _steps maps step names to step objects
// A step object is { accepts: [...], promises: [...] }
, _steps: {}
- // _routes maps http methods (get, post) to route aliases to
- // sequence instances.
- , _routes: {
- get: {}
- , post: {}
- }
+ , _stepSequences: {}
// _configurable maps parameter names to descriptions
// It is used for introspection with this.configurable()
, _configurable: {}
};
+Object.defineProperty(everyModule, '_routes', { get: function () {
+ var seqs = this._stepSequences
+ , methods = ['get', 'post'];
+ return Object.keys(seqs).filter( function (seqName) {
+ return ~methods.indexOf(seqName.split(':')[0]);
+ }).reduce( function (_routes, routeName) {
+ var parts = routeName.split(':')
+ , method = parts[0]
+ , routeAlias = parts[1];
+ _routes[method] || (_routes[method] = {});
+ _routes[method][routeAlias] = seqs[routeName];
+ return _routes;
+ }, {});
+}});
+
+Object.defineProperty(everyModule, 'route', {
+ get: function () {
+ return this._routes;
+ }
+});
+
Object.defineProperty(everyModule, 'routes', {get: function () {
var arr = []
, _routes = this._routes
@@ -306,12 +313,6 @@ Object.defineProperty(everyModule, 'routes', {get: function () {
return arr;
}});
-Object.defineProperty(everyModule, 'route', {
- get: function () {
- return this._routes;
- }
-});
-
everyModule
.configurable({
moduleTimeout: 'how long to wait per step ' +
@@ -333,7 +334,3 @@ everyModule
.logoutRedirectPath('/');
everyModule.moduleTimeout(4000);
-
-everyModule.defaultCondition( function () {
- return true;
-});
View
6 lib/facebook.js
@@ -20,14 +20,10 @@ oauthModule.submodule('facebook')
return this._scope && this.scope();
})
- .callbackDidErr( function (req, res) {
+ .authCallbackDidErr( function (req) {
var parsedUrl = url.parse(req.url, true);
return parsedUrl.query && !!parsedUrl.query.error;
})
- .callbackDidSucceed( function (req, res) {
- var parsedUrl = url.parse(req.url, true);
- return parsedUrl.query && !!parsedUrl.query.code;
- })
.handleAuthCallbackError( function (req, res) {
var parsedUrl = url.parse(req.url, true)
, errorDesc = parsedUrl.query.error_description;
View
67 lib/oauth2.js
@@ -30,6 +30,8 @@ everyModule.submodule('oauth2')
, myHostname: 'e.g., http://local.host:3000 . Notice no trailing slash'
, redirectPath: 'Where to redirect to after a failed or successful OAuth authorization'
, convertErr: 'a function (data) that extracts an error message from data arg, where `data` is what is returned from a failed OAuth request'
+
+ , authCallbackDidErr: 'Define the condition for the auth module determining if the auth callback url denotes a failure. Returns true/false.'
})
// Declares a GET route that is aliased
@@ -46,38 +48,39 @@ everyModule.submodule('oauth2')
.promises(null)
.get('callbackPath',
'the callback path that the 3rd party OAuth provider redirects to after an OAuth authorization result - e.g., "/auth/facebook/callback"')
- .if('callbackDidErr')
+ .step('getCode')
+ .description('retrieves a verifier code from the url query')
+ .accepts('req res')
+ .promises('code')
+ .canBreakTo('authCallbackErrorSteps')
+ .step('getAccessToken')
+ .accepts('code')
+ .promises('accessToken extra')
+ .step('fetchOAuthUser')
+ .accepts('accessToken')
+ .promises('oauthUser')
+ .step('getSession')
+ .accepts('req')
+ .promises('session')
+ .step('findOrCreateUser')
+ //.optional()
+ .accepts('session accessToken extra oauthUser')
+ .promises('user')
+ .step('compile')
+ .accepts('accessToken extra oauthUser user')
+ .promises('auth')
+ .step('addToSession')
+ .accepts('session auth')
+ .promises(null)
+ .step('sendResponse')
+ .accepts('res')
+ .promises(null)
+
+ .stepseq('authCallbackErrorSteps')
.step('handleAuthCallbackError',
'a request handler that intercepts a failed authorization message sent from the OAuth2 provider to your service. e.g., the request handler for "/auth/facebook/callback?error_reason=user_denied&error=access_denied&error_description=The+user+denied+your+request."')
.accepts('req res')
.promises(null)
- .if('callbackDidSucceed')
- .step('getCode')
- .description('retrieves a verifier code from the url query')
- .accepts('req res')
- .promises('code')
- .step('getAccessToken')
- .accepts('code')
- .promises('accessToken extra')
- .step('fetchOAuthUser')
- .accepts('accessToken')
- .promises('oauthUser')
- .step('getSession')
- .accepts('req')
- .promises('session')
- .step('findOrCreateUser')
- //.optional()
- .accepts('session accessToken extra oauthUser')
- .promises('user')
- .step('compile')
- .accepts('accessToken extra oauthUser user')
- .promises('auth')
- .step('addToSession')
- .accepts('session auth')
- .promises(null)
- .step('sendResponse')
- .accepts('res')
- .promises(null)
.getAuthUri( function (req, res) {
var params = {
@@ -119,6 +122,9 @@ everyModule.submodule('oauth2')
})
.getCode( function (req, res) {
var parsedUrl = url.parse(req.url, true);
+ if (this._authCallbackDidErr(req)) {
+ return this.breakTo('authCallbackErrorSteps', req, res);
+ }
return parsedUrl.query && parsedUrl.query.code;
})
.getAccessToken( function (code) {
@@ -185,11 +191,8 @@ everyModule.submodule('oauth2')
res.end();
})
- .callbackDidErr( function (req, res) {
+ .authCallbackDidErr( function (req, res) {
return false;
- })
- .callbackDidSucceed( function (req, res) {
- return true;
});
oauth2.moreAuthQueryParams = {};
View
36 lib/password.js
@@ -21,6 +21,7 @@ everyModule.submodule('password')
.step('displayLogin')
.accepts('req res')
.promises(null)
+
.post('postLoginPath', "the uri path that the login POSTs to. Same as the 'action' field of the login <form />.")
.step('extractLoginPassword')
.accepts('req res')
@@ -73,12 +74,41 @@ everyModule.submodule('password')
res.end(this.registerView());
}
})
+
.post('postRegisterPath', "the uri path that the registration POSTs to. Same as the 'action' field of the registration <form />.")
.step('extractLoginPassword') // Re-used (/search for other occurence)
+ .step('extractExtraRegistrationParams')
+ .description('Extracts additonal query or body params from the ' +
+ 'incoming request and returns them in the `extraParams` object')
+ .accepts('req')
+ .promises('extraParams')
+ .step('validateRegistration')
+ .description('Validates the registration parameters. Default includes check for existing user')
+ .accepts('login password extraParams req res')
+ .promises('newUserAttributes')
+ .canBreakTo('registrationError') // canGoTo
.step('registerUser')
- .description('Creates and returns a new user with login + password')
- .accepts('login password')
+ .description('Creates and returns a new user with newUserAttributes')
+ .accepts('newUserAttributes')
.promises('user')
.step('getSession')
.step('addToSession')
- .step('sendResponse');
+ .step('sendResponse')
+ .extractExtraRegistrationParams( function (req) {
+ return {};
+ })
+ .validateRegistration( function (login, password, extraParams) {
+ if (login && password) return { login: login, password: password };
+ else return this.breakTo('registrationError');
+ // TODO Add in extra args to breakTo
+ })
+
+ .stepseq('registrationError')
+ .step('handleRegistrationError')
+ .accepts('req res errorMessage')
+ .promises(null)
+ .handleRegistrationError( function (req, res, errorMessage) {
+ res.render(this.registerView(), {
+ errorMessage: errorMessage
+ });
+ });
View
26 lib/promise.js
@@ -1,4 +1,4 @@
-var Promise = module.exports = function (values) {
+var Promise = function (values) {
this._callbacks = [];
this._errbacks = [];
this._timebacks = [];
@@ -33,6 +33,7 @@ Promise.prototype = {
return this;
}
, fulfill: function () {
+// console.log(new Error().stack);
if (this._timeout) clearTimeout(this._timeout);
var callbacks = this._callbacks;
this.values = arguments;
@@ -65,3 +66,26 @@ Promise.prototype = {
return this;
}
};
+
+var ModulePromise = module.exports = function (_module, values) {
+ if (values)
+ Promise.call(this, values);
+ else
+ Promise.call(this);
+ this.module = _module;
+};
+
+ModulePromise.prototype.__proto__ = Promise.prototype;
+
+ModulePromise.prototype.breakTo = function (seqName) {
+ if (this._timeout) clearTimeout(this._timeout);
+
+ var args = Array.prototype.slice.call(arguments, 1);
+ var _module = this.module
+ , seq = _module._stepSequences[seqName];
+ if (_module.everyauth.debug)
+ console.log('breaking out to ' + seq.name);
+ seq = seq.materialize();
+ seq.start.apply(seq, args);
+ // TODO Garbage collect the abandoned sequence
+};
View
17 lib/routeTriggeredSequence.js
@@ -0,0 +1,17 @@
+var StepSequence = require('./stepSequence');
+
+var RouteTriggeredSequence = module.exports = function RouteTriggeredSequence (name, _module) {
+ StepSequence.call(this, name, _module);
+}
+
+RouteTriggeredSequence.prototype.__proto__ = StepSequence.prototype;
+
+RouteTriggeredSequence.prototype.routeHandler = function () {
+ // Create a shallow clone, so that
+ // seq.values are different per
+ // HTTP request
+ var seq = this.materialize();
+ // Kicks off a sequence of steps based on
+ // a route.
+ seq.start.apply(seq, arguments); // BOOM!
+};
View
138 lib/step.js
@@ -0,0 +1,138 @@
+var Promise = require('./promise')
+ , clone = require('./utils').clone;
+
+var Step = module.exports = function Step (name, _module) {
+ this.name = name;
+
+ // defineProperty; otherwise,
+ // clone will overflow when we
+ // clone a module
+ Object.defineProperty(this, 'module', {
+ value: _module
+ });
+};
+
+Step.prototype = {
+ /**
+ * @returns {Promise}
+ */
+ exec: function (seq) {
+ var accepts = this.accepts
+ , promises = this.promises
+ , block = this.block
+ , _module = this.module
+ , self = this;
+
+ if (this.debug)
+ console.log('starting step - ' + this.name);
+
+ var args = this._unwrapArgs(seq);
+
+ try {
+ // Apply the step logic
+ ret = block.apply(_module, args);
+ } catch (breakTo) {
+ // Catch any sync breakTo's if any
+ if (breakTo.isSeq) {
+ console.log("breaking out to " + breakTo.name);
+ breakTo.start.apply(breakTo, breakTo.initialArgs);
+ // TODO Garbage collect the promise chain
+ return;
+ } else {
+ // Else, we have a regular exception
+ throw breakTo;
+ }
+ }
+
+ if (promises && promises.length &&
+ 'undefined' === typeof ret) {
+ throw new Error('Step ' + this.name + ' of `' + _module.name +
+ '` is promising: ' + promises.join(', ') +
+ ' ; however, the step returns nothing. ' +
+ 'Fix the step by returning the expected values OR ' +
+ 'by returning a Promise that promises said values.');
+ }
+ // Convert return value into a Promise
+ // if it's not yet a Promise
+ ret = (ret instanceof Promise)
+ ? ret
+ : Array.isArray(ret)
+ ? this.module.Promise(ret)
+ : this.module.Promise([ret]);
+
+ ret.callback( function () {
+ if (seq.debug)
+ console.log('...finished step');
+ });
+
+ var convertErr = _module._convertErr;
+ if (convertErr) {
+ var oldErrback = ret.errback;
+ ret.errback = function (fn, scope) {
+ var oldFn = fn;
+ fn = function (err) {
+ if (! (err instanceof Error)) {
+ err = convertErr(err);
+ }
+ return oldFn.call(this, err);
+ };
+ return oldErrback.call(this, fn, scope);
+ };
+ }
+ // TODO Have a global errback that is configured
+ // instead of using this one.
+ ret.errback( function (err) {
+ throw err;
+ });
+
+ ret.callback( function () {
+ // Store the returned values
+ // in the sequence's state via seq.values
+ var returned = arguments
+ , vals = seq.values;
+ if (promises !== null) promises.forEach( function (valName, i) {
+ vals[valName] = returned[i];
+ });
+ });
+
+ ret.timeback( function () {
+ ret.fail(new Error('Step ' + self.name + ' of `' + _module.name + '` module timed out.'));
+ });
+
+ var timeoutMillis = this.timeout ||
+ _module.moduleTimeout();
+ ret.timeout(timeoutMillis);
+
+ return ret;
+ }
+ /**
+ * Unwraps values (from the sequence) based on
+ * the step's this.accepts spec.
+ */
+ , _unwrapArgs: function (seq) {
+ return this.accepts.reduce( function (args, accept) {
+ args.push(seq.values[accept]);
+ return args;
+ }, []);
+ }
+ , clone: function (name, _module) {
+ var ret = new Step(name, _module);
+ ret.accepts = clone(this.accepts);
+ ret.promises = clone(this.promises);
+ ret.description = this.description;
+ ret.timeout = this.timeout;
+ return ret;
+ }
+};
+
+Object.defineProperty(Step.prototype, 'block', {
+ get: function () {
+ return this._block || (this._block = this.module[this.name]());
+ }
+});
+
+Object.defineProperty(Step.prototype, 'debug', {
+ get: function () {
+ return this.module.everyauth.debug;
+ }
+});
View
188 lib/sequence.js → lib/stepSequence.js
@@ -1,145 +1,97 @@
var Promise = require('./promise')
, clone = require('./utils').clone;
-function MaterializedSequence (_module) {
- this.module = _module;
- // Our sequence state is encapsulated in seq.values
- this.values = {};
- this.orderedStepNames = [];
-}
-
-MaterializedSequence.prototype = {
- clone: function (submodule) {
- var ret = new MaterializedSequence(submodule);
- ret.orderedStepNames = clone(this.orderedStepNames);
- return ret;
- }
- , routeHandler: function () {
- var seq = this
- , steps = this.steps
- , firstStep = steps[0];
-
- // This kicks off a sequence of steps based on a route.
- // Creates a new chain of promises and exposes the leading promise
- // to the incoming (req, res) pair from the route handler
- var args = Array.prototype.slice.call(arguments, 0);
- firstStep.accepts.forEach( function (valName, i) {
- // Map the incoming arguments to the named parameters
- // that the first step is expected to accept.
- seq.values[valName] = args[i];
- });
-
- seq.start(); // BOOM!
- }
+var materializedMethods = {
+ isSeq: true
/**
- * @param {Object} step
+ * Sets up the immediate or eventual
+ * output of priorPromise to pipe to
+ * the nextStep's promise
+ * @param {Promise} priorPromise
+ * @param {Step} nextStep
* @returns {Promise}
*/
- , applyStep: function (step) {
- var accepts = step.accepts
- , promises = step.promises
- // TODO step.block getter?
- , block = this.module[step.name]()
- , seq = this;
-
- if (this.debug)
- console.log('starting step - ' + step.name);
-
- // Unwrap values based on step.accepts
- var args = accepts.reduce( function (args, accept) {
- args.push(seq.values[accept]);
- return args;
- }, []);
- // Apply the step logic
- ret = block.apply(this.module, args);
-
- if (step.promises && step.promises.length &&
- 'undefined' === typeof ret) {
- throw new Error('Step ' + step.name + ' of `' + this.module.name +
- '` is promising: ' + promises.join(', ') +
- ' ; however, the step returns nothing. ' +
- 'Fix the step by returning the expected values OR ' +
- 'by returning a Promise that promises said values.');
- }
- // Convert return value into a Promise
- // if it's not yet a Promise
- if (! (ret instanceof Promise)) {
- if (Array.isArray(ret))
- ret = new Promise(ret);
- else
- ret = new Promise([ret]);
- }
-
- var convertErr = this.module._convertErr;
- if (convertErr) {
- var oldErrback = ret.errback;
- ret.errback = function (fn, scope) {
- var oldFn = fn;
- fn = function (err) {
- if (! (err instanceof Error)) {
- err = convertErr(err);
- }
- return oldFn.call(this, err);
- };
- return oldErrback.call(this, fn, scope);
- };
- }
- // TODO Have a global errback that is configured
- // instead of using this one.
- ret.errback( function (err) {
- throw err;
- });
-
- var seq = this;
- ret.callback( function () {
- // Store the returned values
- // in the sequence's state via seq.values
- var returned = arguments
- , vals = seq.values;
- if (promises !== null) promises.forEach( function (valName, i) {
- vals[valName] = returned[i];
- });
- });
-
- ret.timeback( function () {
- ret.fail(new Error('Step ' + step.name + ' of `' + seq.module.name + '` module timed out.'));
- });
-
- var timeoutMillis = step.timeout ||
- this.module.moduleTimeout();
- ret.timeout(timeoutMillis);
-
- return ret;
- }
- , _bind: function (priorPromise, step) {
- var nextPromise = new Promise()
+ , _bind: function (priorPromise, nextStep) {
+ var nextPromise = this.module.Promise()
, seq = this;
// TODO Have a global errback that is configured
// instead of using this one.
nextPromise.errback( function (err) {
throw err;
});
+
priorPromise.callback( function () {
- if (seq.debug)
- console.log('...finished step');
-
- seq.applyStep(step).callback( function () {
+ var resultPromise = nextStep.exec(seq);
+ if (!resultPromise) return; // if we have a breakTo
+ resultPromise.callback( function () {
nextPromise.fulfill();
- });
+ }); // TODO breakback
});
return nextPromise;
}
+
+ /**
+ * This kicks off a sequence of steps.
+ * Creates a new chain of promises and exposes the leading promise
+ * to the incoming (req, res) pair from the route handler
+ */
, start: function () {
var steps = this.steps;
+ this._transposeArgs(arguments);
+
// Pipe through the steps
- var priorStepPromise = this.applyStep(steps[0]);
+ var priorStepPromise = steps[0].exec(this);
+
+ // If we have a breakTo
+ if (!priorStepPromise) return;
+
for (var i = 1, l = steps.length; i < l; i++) {
priorStepPromise = this._bind(priorStepPromise, steps[i]);
}
return priorStepPromise;
}
+ /**
+ * Used for exposing the leading promise
+ * of a step promise chain to the incoming
+ * args (e.g., [req, res] pair from the
+ * route handler)
+ */
+ , _transposeArgs: function (args) {
+ var firstStep = this.steps[0]
+ , seq = this;
+ firstStep.accepts.forEach( function (paramName, i) {
+ // Map the incoming arguments to the named parameters
+ // that the first step is expected to accept.
+ seq.values[paramName] = args[i];
+ });
+ }
+};
+
+var StepSequence = module.exports = function StepSequence (name, _module) {
+ this.name = name;
+ this.module = _module;
+ this.orderedStepNames = [];
+}
+
+StepSequence.prototype = {
+ constructor: StepSequence
+ , clone: function (submodule) {
+ var ret = new (this.constructor)(this.name, submodule);
+ ret.orderedStepNames = clone(this.orderedStepNames);
+ return ret;
+ }
+
+ , materialize: function () {
+ var ret = Object.create(this);
+ ret.values = {};
+ for (var k in materializedMethods) {
+ ret[k] = materializedMethods[k];
+ }
+ return ret;
+ }
+
// TODO Replace logic here with newer introspection code
, checkSteps: function () {
var steps = this.steps
@@ -174,7 +126,7 @@ MaterializedSequence.prototype = {
}
};
-Object.defineProperty(MaterializedSequence.prototype, 'steps', {
+Object.defineProperty(StepSequence.prototype, 'steps', {
get: function () {
// Compile the steps by pulling the step names
// from the module
@@ -254,10 +206,8 @@ Object.defineProperty(MaterializedSequence.prototype, 'steps', {
}
});
-Object.defineProperty(MaterializedSequence.prototype, 'debug', {
+Object.defineProperty(StepSequence.prototype, 'debug', {
get: function () {
return this.module.everyauth.debug;
}
});
-
-module.exports = MaterializedSequence;

0 comments on commit 1fded46

Please sign in to comment.