Skip to content
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

Add Router events #49

Closed
wants to merge 1 commit into from
Closed

Conversation

hacksparrow
Copy link
Member

@hacksparrow hacksparrow commented Oct 24, 2016

The following events were added:

  1. handlestart - emitted when the router starts to handle the router stack.
  2. layer - emitted when a layer is matched by the router. It returns the matched layer.
  3. handleend - emitted when the router has completed handling the router stack.

This feature should help developers handle requirements as described in expressjs/express#2501, expressjs/express#3100, and similar.

Example of usage with Express:

app.router.on('handlestart', function () {
  console.log('------- %d -------', Date.now())
})

app.router.on('layer', function (layer) {
  console.log(layer)
})

app.router.on('handleend', function () {
  console.log('------- %d -------', Date.now())
})

@dougwilson @danieljuhl @evanshortiss \o/

@evanshortiss
Copy link

@hacksparrow this is nice. I guess "layer" would be the event I'm looking for, but where can I associate that to the request itself here? For example, being able to do the below, would be nice, but needs req to be injected:

app.router.on('layer', function (layer, req) {
  req.matchedLayers = req.matchedLayers ? req.matchedLayers.concat([layer]) : [layer]
})

@evanshortiss
Copy link

The end goal being that I can have a middleware like so:

function routeLogger (req, res, next) {
  res.on('finish', function () {
    // write to analytics, or log route
    console.log(req.matchedLayers); // Maybe do some formatting here to get the actual paths joined
  });
}

@dougwilson
Copy link
Contributor

We will also need to discuss how the events are bubbled (or not). For example, would a user be required to attach an event listener on every single router (and of course, understand the list of all their routers) or should this use a form of event bubbling between routers, similar to the DOM?

@hacksparrow
Copy link
Member Author

hacksparrow commented Oct 26, 2016

@evanshortiss great suggestion. "handlestart" and "handleend" would need the request context too.

@dougwilson excellent feedback. I will implement a bubbling version as well for further thoughts on the feature.

@hacksparrow hacksparrow force-pushed the router-events branch 3 times, most recently from 5756cca to 585eeb0 Compare October 27, 2016 16:28
@hacksparrow
Copy link
Member Author

hacksparrow commented Oct 27, 2016

@evanshortiss the event handlers have been updated to the following:

app.router.on('handlestart', function (req, router) {

})

app.router.on('layer', function (layer, req, router) {

})

app.router.on('handleend', function (req, router) {

})

@evanshortiss
Copy link

evanshortiss commented Oct 27, 2016

That looks solid to me. Should provide plenty of context for introspection. Nice work! 👍

@dougwilson
Copy link
Contributor

@hacksparrow why add the router to the event? Isn't that available within the event handler as the context (this)?

@hacksparrow
Copy link
Member Author

@dougwilson err yes! I will remove it from the params. Thanks.

@hacksparrow
Copy link
Member Author

hacksparrow commented Oct 27, 2016

So, just for the record, the signature now is:

app.router.on('handlestart', function (req) {

})

app.router.on('layer', function (layer, req) {

})

app.router.on('handleend', function (req) {

})

Copy link
Contributor

@dougwilson dougwilson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional to the other comments, any thoughts about the DOM-like event bubbling?

index.js Outdated
@@ -186,6 +192,9 @@ Router.prototype.handle = function handle(req, res, callback) {
req.baseUrl = parentUrl
req.originalUrl = req.originalUrl || req.url

// trigger the "beginning of route handling" event
if (events && routerEvents[0] in events) self.emit('handlestart', req)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is routerEvents[0] in events actually saving any time? I'm not sure if accessing _events and using in on it is really a good long term API to use with event emitters.

Copy link
Member Author

@hacksparrow hacksparrow Nov 3, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I decided on this to keep the naming convention "flexible". Will come up with a better alternative.

index.js Outdated
@@ -269,6 +278,9 @@ Router.prototype.handle = function handle(req, res, callback) {
return done(layerError)
}

// trigger the "layer found" event
if (events && routerEvents[1] in events) self.emit(routerEvents[1], layer, req)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we swap req and layer, all three events will conveniently start with req as the first argument.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good suggestion 👍

test/router.js Outdated
var server = createServer(router)

router.on('handlestart', function (req) {
assert(IncomingMessage.prototype.isPrototypeOf(req))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just check for certain properties, perhaps add a request header and try to read it here, rather than checking the prototype specifically. I know there are a bunch of people interested in better browser support, so if there is no reason to hurt the support, I would err on the side of that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

test/router.js Outdated
var counter = 0
router.on('layer', function (layer, req) {
counter++
if (handlers.length === counter) done()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend using the after function for this type of tracking, which will throw when invoked too much.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@dougwilson
Copy link
Contributor

Does anyone have any thoughts on the event names? I'm neutral on them :)

@crandmck
Copy link

crandmck commented Nov 3, 2016

Please add some docs for this to the README.

The following events were added:

1. `handlestart` - emitted when the router starts to handle the
router stack.
2. `layer` - emitted when a layer is macthed by the router. It
returns the matched layer.
3. `handleend` - emitted when the router has completed handling
the router stack.

I am sure very sure about the name of the events, feedback are
most welcome.
@hacksparrow
Copy link
Member Author

@dougwilson @crandmck I have updated the PR according to your suggestions.

For the record, the signature of the layer event listener now is:

app.router.on('layer', function (req, layer) {

})

@crandmck
Copy link

crandmck commented Nov 11, 2016

Thanks, docs LGTM.

@wesleytodd
Copy link
Member

wesleytodd commented Nov 13, 2016

Anything blocking this? I would love to replace what I have been doing in middleware with these new events.

@dougwilson
Copy link
Contributor

There are still issues I wrote above unaddressed, with the biggest that it is reaching into the internals of Node.js events which is unlikely to keep working in the future.

@wesleytodd
Copy link
Member

Ahh, sorry @dougwilson I didn't open the "outdated diffs". I completely agree with the comments, and am anxiously awaiting this merge :) Thanks for the good work @hacksparrow!!


// trigger the "end of route handling" event
if (events && 'handleend' in events) {
res.once('finish', function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of adding the finish event listener? Especially because there are no tests around all the edge cases this will cause (for example, are we saying that if the response is already finished before the handleend, our events is not supposed to fire?)

@@ -164,6 +168,7 @@ Router.prototype.handle = function handle(req, res, callback) {
var self = this
var slashAdded = false
var paramcalled = {}
var events = self._events
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not peak into the internals of Node.js if it's not required, and I see no reason this is required. If there is, please enlighten me :)

@@ -20,6 +20,7 @@ var mixin = require('utils-merge')
var parseUrl = require('parseurl')
var Route = require('./lib/route')
var setPrototypeOf = require('setprototypeof')
var EventEmitter = require('events').EventEmitter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be alphabetical like the rest.


### 2. `layer`

This event is emitted when a route layer is matched.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you define "route layer" in the docs?

})
```

Here is a complete example of using router events in an Express 5 app.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be part of the handleend section.

Here is a complete example of using router events in an Express 5 app.

```js
var express = require('express')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the example be a real example instead of just a bunch of console log lines? I.e. demonstrate the use case people will use this for. People love to copy paste, and this is not something they would do that on.

@@ -983,8 +984,153 @@ describe('Router', function () {
.expect(200, 'saw GET /bar', done)
})
})

describe('events', function () {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests have a very inconsistent style from all other tests if you can clean them up at some point :)

@dougwilson
Copy link
Contributor

Also, what ever happened with the whole event bubbling discussion? I feel like I've mentioned it before, and @hacksparrow said he would look into it, but never really anything seemed to happen there. So what is the thoughts on that? Does that make it easier to accomplish whatever the goals of this PR are or not?

@hacksparrow
Copy link
Member Author

@dougwilson I haven't made significant progress on event bubbling yet. Will update once done. In the mean time, it'd be great to hear the community's opinion about a bubbling and non-bubbling version.

@evanshortiss
Copy link

@dougwilson @hacksparrow just to be clear, by bubbling you mean bubble to an upper "app" (express) instance? So for submounted apps, the event will bubble to the parent app and so on?

@hacksparrow
Copy link
Member Author

@evanshortiss bubbling within the routers (independent or the one that comes with Express) hierarchy. We were thinking along lines of the browser DOM events.

@wesleytodd
Copy link
Member

wesleytodd commented Nov 17, 2016

TBH I think you have to do bubbling for layer events, if you don't I think the behavior will be unexpected. That being said, multiple start and end events seem problematic, or atleast overly complex. I see two options on that:

  1. "Bubbled" events get prefixed, or a different name altogether
  2. handlestart/handleend events don't bubble, but layer events still do

@wesleytodd
Copy link
Member

To address the cases that I would use this for right away, here are two I can think of:

  1. Unbind events on the front-end when the NEXT handlestart event happens
  2. Monitoring performance metrics for each layer

So for number one, having multiple start events for one "run" of the router stack, would mean I would have to track it myself. Possible, obviously, but it would be nicer if the design compensated for this and gave me a simple way.

For number two, each "layer", IMO should include all sub routers. Thus requiring bubbling.

@wesleytodd
Copy link
Member

wesleytodd commented Mar 25, 2017

So I am circling back on this, and after re-thinking it I do not think anything other than the layer event should be included. Here are my thoughts:

  1. There are complexities this introduced, as discussed above in detail. None of which have simple resolutions.

    • Bubbling: the semantics of this are hard to get right for start and end, but pretty clear for layer
    • Handle End: Currently it is the response end, but this can already be handled with the on-finished package, and the router end has little to no value.
  2. The feature's we have all mentioned can be solved in other ways:

    • Original Route/Matched Route: Store matched routes in request #34 solves this in a much better way
    • Metrics: If you actually care to get accurate metrics you should be capturing the start time sooner than this, and as mentioned above, handleend is an alias for onFinished(res, cb).
    • Logging: Because Store matched routes in request #34 gets you all the info you need, you can then just log in a middleware at the end of your stack

That all being said, the one thing we cannot do with ease is track stats and metrics on individual layers. Which is why I propose that this change just to:

  • layerstart: Same as the current layer event is now
  • layerend: Added once next is called after matching a layer

So I created #56 to illustrate and keep the discussion going.

@fcolella
Copy link

fcolella commented Mar 6, 2019

Hi guys! so is this still happening? :)

@wesleytodd
Copy link
Member

Hi guys! so is this still happening? :)

If it is, as I said in my last comment, I would hope we just go with the layer events. That being said, I am not sure even this is the best way forward, and I have an idea which has been bubbling about adding a decorator mechanism which would allow for much more flexibility and also superseded the need to add these events at all.

I have left open my other PR since it might be good, but I think this PR should be closed. If no one disagrees (@dougwilson or @hacksparrow) then I will close this in a week (reminder already set to revisit).

@wesleytodd
Copy link
Member

Ok, no comments. But @dougwilson it looks like I am not an owner on this org so cannot close this. 🤷‍♂️

@wesleytodd
Copy link
Member

@dougwilson It looks like just adding me to the tc group is not enough to be able to close issues. Seems like there must be some settings to make that work, but I am not sure.

@dougwilson
Copy link
Contributor

Sorry @wesleytodd about that. I will check it out and make sure your permissions are in order. You should have the ability do to this, so I will check why here shortly 👍

@wesleytodd
Copy link
Member

Finally coming back around here. This is superseded by #56 IIRC. Closing this, and assessing if that could possibly land for router@2 and express@5.

@wesleytodd wesleytodd closed this Mar 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants