-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(middleware): Return a promise that resolves when middleware stack done #1339
Changes from 7 commits
29fba0f
a0cc04e
2806278
12bdbfa
944f292
622becd
d92385e
59a74b5
903615f
58d3de5
76b0d61
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
async = require 'async' | ||
require('es6-promise').polyfill() | ||
|
||
class Middleware | ||
# We use this recursively, and using nextTick recursively is deprecated in node 0.10. | ||
|
@@ -21,32 +22,44 @@ class Middleware | |
# done() - Initial (final) completion callback. May be wrapped by | ||
# executed middleware. | ||
# | ||
# Returns nothing | ||
# Returns promise - resolves with context when middleware completes | ||
# Returns before executing any middleware | ||
execute: (context, next, done) -> | ||
done ?= -> | ||
# Execute a single piece of middleware and update the completion callback | ||
# (each piece of middleware can wrap the 'done' callback with additional | ||
# logic). | ||
executeSingleMiddleware = (doneFunc, middlewareFunc, cb) => | ||
# Match the async.reduce interface | ||
nextFunc = (newDoneFunc) -> cb(null, newDoneFunc or doneFunc) | ||
# Catch errors in synchronous middleware | ||
try | ||
middlewareFunc.call(undefined, context, nextFunc, doneFunc) | ||
catch err | ||
# Maintaining the existing error interface (Response object) | ||
@robot.emit('error', err, context.response) | ||
# Forcibly fail the middleware and stop executing deeper | ||
doneFunc() | ||
|
||
# Executed when the middleware stack is finished | ||
allDone = (_, finalDoneFunc) -> next(context, finalDoneFunc) | ||
new Promise (resolve, reject) => | ||
|
||
# Execute each piece of middleware, collecting the latest 'done' callback | ||
# at each step. | ||
process.nextTick => | ||
async.reduce(@stack, done, executeSingleMiddleware, allDone) | ||
done ?= -> | ||
|
||
# Allow each middleware to resolve the promise early if it calls done() | ||
pieceDone = -> | ||
done() | ||
resolve context | ||
|
||
# Execute a single piece of middleware and update the completion callback | ||
# (each piece of middleware can wrap the 'done' callback with additional | ||
# logic). | ||
executeSingleMiddleware = (doneFunc, middlewareFunc, cb) => | ||
# Match the async.reduce interface | ||
nextFunc = (newDoneFunc) -> cb(null, newDoneFunc or doneFunc) | ||
# Catch errors in synchronous middleware | ||
try | ||
middlewareFunc.call(undefined, context, nextFunc, doneFunc) | ||
catch err | ||
# Maintaining the existing error interface (Response object) | ||
@robot.emit('error', err, context.response) | ||
# Forcibly fail the middleware and stop executing deeper | ||
doneFunc() | ||
reject err, context | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Promise.reject(new Error('foo'), {}).catch(function (error, context) {
console.log(context)
})
// logs `undefined` What you could do is set context as property of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good one. Thanks for that info, I'll do it that way instead. |
||
|
||
# Executed when the middleware stack is finished | ||
allDone = (_, finalDoneFunc) -> | ||
next context, finalDoneFunc | ||
resolve context | ||
|
||
# Execute each piece of middleware, collecting the latest 'done' callback | ||
# at each step. | ||
process.nextTick => | ||
async.reduce(@stack, pieceDone, executeSingleMiddleware, allDone) | ||
|
||
# Public: Registers new middleware | ||
# | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -208,6 +208,64 @@ describe 'Middleware', -> | |
allDone | ||
) | ||
|
||
it 'returns a promise that resolves when async middleware stack is complete', (testDone) -> | ||
|
||
testMiddlewareA = (context, next, done) -> | ||
setTimeout -> | ||
context.A = 'done' | ||
next(done) | ||
, 50 | ||
|
||
testMiddlewareB = (context, next, done) -> | ||
setTimeout -> | ||
context.B = 'done' | ||
next(done) | ||
, 50 | ||
|
||
@middleware.register testMiddlewareA | ||
@middleware.register testMiddlewareB | ||
|
||
middlewareFinished = -> | ||
|
||
middlewarePromise = @middleware.execute( | ||
{} | ||
(_, done) -> done() | ||
middlewareFinished | ||
) | ||
|
||
middlewarePromise.then (finalContext) -> | ||
expect(finalContext).to.eql A: 'done', B: 'done' | ||
testDone() | ||
|
||
it 'promise resolves when middleware completes early, with context at that point', (testDone) -> | ||
|
||
testMiddlewareA = (context, next, done) -> | ||
setTimeout -> | ||
context.A = 'done' | ||
done() | ||
, 50 | ||
|
||
testMiddlewareB = (context, next, done) -> | ||
setTimeout -> | ||
context.B = 'done' | ||
next(done) | ||
, 50 | ||
|
||
@middleware.register testMiddlewareA | ||
@middleware.register testMiddlewareB | ||
|
||
middlewareFinished = -> | ||
|
||
middlewarePromise = @middleware.execute( | ||
{} | ||
(_, done) -> done() | ||
middlewareFinished | ||
) | ||
|
||
middlewarePromise.then (finalContext) -> | ||
expect(finalContext).to.eql A: 'done' | ||
testDone() | ||
|
||
describe 'error handling', -> | ||
it 'does not execute subsequent middleware after the error is thrown', (testDone) -> | ||
middlewareExecution = [] | ||
|
@@ -238,7 +296,7 @@ describe 'Middleware', -> | |
{} | ||
middlewareFinished | ||
middlewareFailed | ||
) | ||
).catch (reason) -> # supress warning re unhandled promise rejection | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this start showing unexpected warnings when people upgrade? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without that line 299, it was logging a warning in the tests, where middleware threw. Saying "unhandled promise rejection" - not a big deal, but simply catching the promise and doing nothing with it removes the warning. It would only effect people who's middleware is also throwing, in which case they'd see errors anyway. |
||
|
||
it 'emits an error event', (testDone) -> | ||
testResponse = {} | ||
|
@@ -263,7 +321,7 @@ describe 'Middleware', -> | |
{response: testResponse}, | ||
middlewareFinished, | ||
middlewareFailed | ||
) | ||
).catch (reason) -> # supress warning re unhandled promise rejection | ||
|
||
it 'unwinds the middleware stack (calling all done functions)', (testDone) -> | ||
extraDoneFunc = null | ||
|
@@ -291,7 +349,7 @@ describe 'Middleware', -> | |
{} | ||
middlewareFinished | ||
middlewareFailed | ||
) | ||
).catch (reason) -> # supress warning re unhandled promise rejection | ||
|
||
describe '#register', -> | ||
it 'adds to the list of middleware', -> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see my other comment, a promise can only reject with one argument