-
Notifications
You must be signed in to change notification settings - Fork 147
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
Wrap each middleware in array with provided middleware #51
Conversation
hah shoot. this is so simple. maybe too simple. 👍 /cc @koajs/logging @koajs @PlasmaPower @jeromew |
@jonathanong I say, why not leave the complexity to a possible function(fn) {
return (ctx, next) => {
const start = process.hrtime()[1]
console.log(' > ' + start)
return Promise.resolve(fn(ctx, next)).then(() => {
const end = process.hrtime()[1] - start
console.log(' < ' + end + ' (' + end/1000000 + ')')
})
}
} Edit removed fn.name for obvious fat-arrow reasons. |
@PlasmaPower also raised concern regarding if wrapper should be an |
LGTM |
This might be a bit crazy, but what if we had app.use(static('/')); // This wouldn't be profiled
app.use(profiler()); // Attach the profiler
app.use(router.routes()); // This would be profiled
app.use(mount('/other', static('/other'))); // Both mount and static would be profiled The profiler could even be mounted to routes. It could observe the overhead of Another use of this is if you wanted to, for example, use your own type of middleware (we have generators, promises, who knows what else might come up). If we create wrappers as an array, it could coexist with a profiler. It would have to come before the profiler though. We could mitigate that if we wanted with multiple stages, but I don't think it would be worth it. The one disadvantage of this is that we have to wrap middleware at "runtime" (or "requesttime" :P). However, given that this PR is already doing that and nobody mentioned anything, I'd say it's not a problem (yes I know it's a proof of concept but still). In addition, wrappers shouldn't take too long to do the wrapping, the added cost of running the middleware will be the main performance cost. If we do merge this PR as-is though, I'd prefer if we wrapped the middleware before hand. |
@PlasmaPower'PR is for koa2 and I agree that it is now implemented as a runtime wrap ; it could be modified with middlewares wrapped earlier as I see. Only a perf test could show the impact of this. Regarding koa1, the impact of a runtime wrapping would be more important I think (but I have no tests to prove anything) but I like the idea of targeting some requests only. Sometimes you simply want to profile a sample of requests, from start to finish. Reserving a property on context for profiling could help for creating targeted profiling (on a request or on parts of the flow). As for a wrapper versus an array of wrapper, is there a difference between
the array could be interesting if there are reasons to maybe modify the array on the fly but I have no experience with the problematic of a stack of wrappers. Maybe this is also linked to the need for introspection in the middleware graph. Once an array of middleware is composed, there is no way to know which middlewares it was composed of. Also, I don't know where we are on mw naming, but I think that a wrapper/profiler should have a clean access to the name of the mw, be it fn.name or fn._name, because middleware graphs can become complicated and profiling a string of anonymous mw can be tiring. |
@jeromew From what I understand, there's a good chance we'll be moving to v2 as default soon. As for arrays, using compose([a, b], fn => wrap1(wrap2(fn))); I think an array would be cleaner. Also, if we're storing it in ctx, then an array would be much cleaner. As for naming, the standard is |
As far as targeting middleware for wrapping, there's nothing stopping user from composing and passing a profiler themselves, i.e. const compose = require('compose')
app.use(someMiddleware) // not profiled
app.use(compose([a, b, ...], profiler)) // profiled
app.use(someOtherMiddleware) // not profiled |
@fl0w Yeah I agree that the targeting of middleware isn't that big of a deal. The main reason I'd like Edit: one thing that solution doesn't address is only profiling |
@PlasmaPower I'm not sure I understand when ctx.wrappers actually would wrap? This would be a The simplicity behind this is that it delegates to user space. I like that, some might want to extend an emitter, or use a "singleton object" for storing additional isolated metric data (which neither context/koa or application cares about). Edit; And if need be, a user can create a stack of functions - as long as the returned root fn is of v2 signature. |
@fl0w it would be implemented in |
@PlasmaPower ok I didn't know that koa2 was so close. btw I was away for quite some time, is there a writeup somewhere that I can read about the rationale of the change from generators to promises ? to me a wrapper is nearly a middleware when it can do something before the mw on the way downstream and after the mw on the way upstream. Is this wrong ? |
@jeromew To be clear; I'm the new guy. This was taken from koajs/koa#219. |
@jeromew You can find some discussion of koa v2 at koajs/koa#533. Profiler wrappers would usually work as if middleware was added before and after every middleware, that's why they are wrappers, they wrap the middleware. However, wrappers could also do things like convert types of middleware to promise based middleware. |
If we implement this, especially if we implement this as an array, I'd be in favor of removing the deprecated generator to promise conversion in favor of |
@PlasmaPower Unless you want to create a PR, I can update PR later today according to your suggestions (as I understand them) later today (CET TZ). Effectively it comes down to
Does this reflect your thoughts? I like that wrappers becomes multi-purpose; It's almost as if they're mw-decorators. |
@fl0w I'll create a separate PR. I wanted a bit of feedback first. |
Yeah #52 is opinionated. However, I also think that it leads to the best experience when using wrappers. IDK. |
@@ -37,7 +39,9 @@ function compose (middleware) { | |||
function dispatch (i) { | |||
if (i <= index) return Promise.reject(new Error('next() called multiple times')) | |||
index = i | |||
const fn = middleware[i] || next | |||
let fn | |||
if (wrapper) fn = middleware[i] ? wrapper(middleware[i]) : next |
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.
haha why not just
let fn = middleware[i] || next
if (wrapper) fn = wrapper(fn)
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.
We don't want to wrap next.
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.
why not?
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.
next
is guaranteed to be a function in conformance with standard Koa practice. It's not coming from the user 90% of the time. If you use co.wrap
as a wrapper, you don't want it processing next.
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.
Oh, also, a profiler shouldn't run on the next function, that just wouldn't make sense.
LGTM too. only features it's missing that should probably be in the framework is:
do you guys think we should do that here or in the framework? |
Well we need tests of course. I'd prefer a wrappers array as previously mentioned.
How/why? I'm not sure I get your point there. |
many times it's just for debugging. you might want to disable it in production. for example, you can enable/disable of course, you could always do something like: const wrappers = process.env.NODE_ENV === 'production' ? (fn) => {} : null but then that might get annoying. it's up to you guys on how you think the best dev UX is. the biggest problem IMO is that people are probably going to nest |
@PlasmaPower compose would get the wrapper from app.wrapper. But I agree, there's no real difference between this and Any other crazy ideas to approach this differently? Have all mw in compose be wrapped in an emitter and just emit pre/post? Force all lib authors to push mw onto Edit This was an issue in 2014 already #6 (comment) |
@fl0w how would compose get the app object though? through the context? then why not skip the app altogether and just store the wrappers in the context? The difference with changing Application.callback to wrap the middleware or having use wrap is that this would hopefully be supported by things like |
hmmm. i see pros and cons with all the implementations. but i think we all agree that the API for the wrapper should just be how about we just create a module that creates a wrapper: const wrap = new Wrapper({
// default `true` always
enabled: process.env.NODE_ENV === 'production',
})
wrap.push(fn => {
return async (ctx, next) => {
console.log('before')
await fn(ctx, next)
console.log('after')
}
}) then using it must be done manually: app.use(wrap(async (ctx, next) => {
this.status = 204
})) this is the least opinionated and the API isn't too bad (a lot of koa modules already wrap functions like this). routers like regardless where what do you guys think? if one of you wanna make it, we can add it to the org and basically make it official |
just realized that |
@jonathanong that could get messy, with some middleware having wrap called and others not because whatever uses them has wrapping builtin. However, if anyone wants to make that, here's a small thing to do it: (off the top of my head, might need troubleshooting) module.exports = function (opts) {
opts = opts || {};
opts.enabled = opts.enabled || true;
var ret = function (fn) {
if (opts.enabled) {
var wrapped = this.wrappers.reduce((mw, wrapper) => wrapper(mw), fn);
wrapped._name = 'wrapped-' + fn.name;
return wrapped;
} else {
return fn;
}
};
ret.wrappers = [];
return ret;
}; |
I'd prefer we have koa, router, etc accept a wrappers array though. We could still have that module as a backup I guess for when something doesn't support it. If you're wondering about enabling with that method, I'd do this: if (process.env.NODE_ENV === 'development') {
app.wrappers.push(profiler);
}
app.use(static('/'));
app.use(router({ wrappers: app.wrappers })); |
so that's just basically |
I feel like there should be a way for the router to get the app without being passed it, I mean it's right there in the same line of code. A As for passing it app, at that point why not get the wrappers from ctx.app at runtime, and at that point why not my idea? I guess |
@PlasmaPower sorry, been AFK. I understand your point now. You're right, I couldn't create a wrapper-lib that grabs wrappers from context with this I dislike having to |
we could also go the route of then have only problem with this is that routers will HAVE to |
and if we memoize the flattened array, then users may not be able to edit the routing stack after |
@jonathanong A router can't simply return a list of it's midlleware, they have to be wrapped by the router first, and at that point wrapping the wrapper is useless. |
@PlasmaPower true, depends on the router. i guess they could always |
@jonathanong I'm pretty sure this would mean that both koa-route and koa-router (the two most popular routers) would need to be redesigned. |
koa-route should be fine because it's one-route/one-method/one-middleware. i just realized koa-router doesn't have the unless i'm missing osmething? |
@jonathanong the problem is that koa-route wraps the middleware itself (source). Wrapping the wrapped middleware won't do anything, the middleware needs to be wrapped before it's wrapped by koa-route. Edit: that's a lot of wrapped :P |
oh i see. i didn't mind jsut wrapping the wrapped middleware. but if that's the case, then we should have |
@jonathanong say you want to use co.wrap as your wrapper - you couldn't do that with this system and koa-route. This would pretty much restrict it to a before and after hook. I'm not sure what you mean by app.wrap(fn), would it be something like the call-middleware repo I made? If so, I'm not sure how much that would help because you'd need the app anyway. The only way to get that with the current koa-route API is through the context at runtime, and at that point we're opinionating it already so we might as well go with my solution. I think another alternative is a .onUsed API, which could pass middleware a number of things, the obvious one being the app it's being used on. Things like routers and compose should call .onUsed with the app they got from their .onUsed. |
yup. i think we have to put it in so koa-compose will use |
@jonathanong yep, anything that calls user middleware should use call-middleware. We could also put the function in app, but I thought a module would be a bit cleaner. |
ok cool. i'm convinced. if u wanna put together all the use-cases and try to get a consensus, taht would be great :) sorry, i've been busy and haven't really been lookinga t open source stuff lately. |
@jonathanong no problem :). Another problem this solves is that I accidentally passed generator middleware to koa-mount, and it didn't warn me, everything just didn't work. A call middleware utility would solve that. I've put together my solution (including call-middleware) in #52. |
I see you guys found consensus, I'm still on the defensive about either solution. Though I'll close this then, as #52's implementation is correct according to your discussion. |
For what it's worth; despite the aggressiveness (breaking) of @jonathanong's proposal, I feel this would be the best solution long-term. |
@fl0w I wouldn't mind @jonathanong's proposal if we implement it using the |
@PlasmaPower I think koajs/koa#707 is a step in the right direction. I'm not sure I understand the implementation yet. It definitely doesn't feel as misplaced (to me) as previous solutions. Would it be possible and make sense to extract koajs/koa#707 (or equivalent) into its own project/package? |
@fl0w we could package prepareMiddleware into it's own package, but it wouldn't matter that much since everybody will end up using prepareMiddleware bound to the useContext. Besides, it's 15 lines of code. Is there really any reason to put it in it's own package? Also, about how This PR implementing it for koa-compose might give you a good understanding Note: that implementation is necessary for Koa since Koa uses compose to call the middleware mw.onUsed and app.prepareMiddlewareMiddleware.onUsed is called by prepareMiddleware. The prepareMiddleware chain is started in callback in callback. The useChainThe useChain is another component of a useContext that I put in so that profiling can have a tree. The useChain is initialized blank, but Edit: I've since changed the useChain to be made up of middleware functions instead of names. |
I'm just getting the ball rolling here.
All tests are passing, though no new tests are written yet.
Probing (no pun intended) to check if this is the direction koajs members want to take this.
Related discussion/issue: koajs/koa#219