diff --git a/README.md b/README.md index cbbe015..18888c0 100644 --- a/README.md +++ b/README.md @@ -332,6 +332,71 @@ curl http://127.0.0.1:8080/such_path > such_path ``` +## Events + +The router emits two events - `layerstart`, and `layerend` - as it processes requests. + +### 1. `layerstart` + +This event is emitted when the router matches a layer, and starts the middleware stack + +Example: + +```js +router.on('layerstart', function (req) { + req.layerStartTime = Date.now() +}) +``` + +### 2. `layerend` + +This event is emitted when a route layer finishes calling middleware functions + +Example: + +```js +router.on('layerend', function (req, res, layer) { + console.log('The layer ' + layer.path + ' took ' + (Date.now() - req.layerStartTime) + 'ms') +}) +``` + +Here is a complete example of using router events in an Express 5 app. + +```js +var express = require('express') +var onFinished = require('on-finished') +var app = express() + +app.use(function (req, res, next) { + req.id = Math.random().toString(36).slice(2) + req.layerCounter = 0 + req.totalTime = 0 + onFinished(res, function logLayerStats (err) { + console.log('Request ' + req.id + ':') + console.log(' Request Errored: ' + !!err) + console.log(' Layers run: ' + req.layerCounter) + console.log(' Avg Time: ' + req.totalTime / req.layerCounter) + }) + + next() +}) + +app.get('/', function (req, res) { + res.send('HELLO') +}) + +app.router.on('layerstart', function (req, res, layer) { + req.layerStartTime = Date.now() +}) + +app.router.on('layerend', function (req, res, layer) { + req.layerCounter++; + req.totalTime += Date.now() - req.layerStartTime; +}) + +app.listen(3000) +``` + ## License [MIT](LICENSE) diff --git a/index.js b/index.js index 05cbd63..a2588f4 100644 --- a/index.js +++ b/index.js @@ -13,6 +13,7 @@ */ var debug = require('debug')('router') +var EventEmitter = require('events').EventEmitter var flatten = require('array-flatten') var Layer = require('./lib/layer') var methods = require('methods') @@ -64,6 +65,9 @@ function Router(options) { router.handle(req, res, next) } + // make Router an EventEmitter + mixin(this, EventEmitter.prototype, false) + // inherit from the correct prototype setPrototypeOf(router, this) @@ -275,6 +279,13 @@ Router.prototype.handle = function handle(req, res, callback) { return done(layerError) } + // trigger the "layer found" event + self.emit('layerstart', req, res, layer) + var _next = function (err) { + self.emit('layerend', req, res, layer) + next(err) + } + // store route for dispatch on change if (route) { req.route = route @@ -289,18 +300,18 @@ Router.prototype.handle = function handle(req, res, callback) { // this should be done for the layer self.process_params(layer, paramcalled, req, res, function (err) { if (err) { - return next(layerError || err) + return _next(layerError || err) } if (route) { return layer.handle_request(req, res, next) } - trim_prefix(layer, layerError, layerPath, path) + trim_prefix(layer, layerError, layerPath, path, _next) }) } - function trim_prefix(layer, layerError, layerPath, path) { + function trim_prefix(layer, layerError, layerPath, path, _next) { if (layerPath.length !== 0) { // Validate path breaks on a path separator var c = path[layerPath.length] @@ -330,9 +341,9 @@ Router.prototype.handle = function handle(req, res, callback) { debug('%s %s : %s', layer.name, layerPath, req.originalUrl) if (layerError) { - layer.handle_error(layerError, req, res, next) + layer.handle_error(layerError, req, res, _next) } else { - layer.handle_request(req, res, next) + layer.handle_request(req, res, _next) } } } @@ -480,6 +491,14 @@ Router.prototype.use = function use(handler) { // add the middleware debug('use %o %s', path, fn.name || '') + // If fn looks like another router instance, + // bubble the layerstart and layerend events + // up the tree + if (typeof fn.use === 'function' && typeof fn.emit === 'function') { + this.on('layerstart', fn.emit.bind(fn, 'layerstart')) + this.on('layerend', fn.emit.bind(fn, 'layerend')) + } + var layer = new Layer(path, { sensitive: this.caseSensitive, strict: false, diff --git a/test/router.js b/test/router.js index ea3d73c..cc3e22e 100644 --- a/test/router.js +++ b/test/router.js @@ -3,6 +3,7 @@ var after = require('after') var methods = require('methods') var Router = require('..') var utils = require('./support/utils') +var Layer = require('../lib/layer.js') var assert = utils.assert var createHitHandle = utils.createHitHandle @@ -1129,8 +1130,89 @@ describe('Router', function () { .expect(200, 'saw GET /bar', done) }) }) + + describe('events', function () { + + describe('"layer"', function () { + it('should pass the request and response objects and the matched layer', function (done) { + var router = new Router() + var server = createServer(router) + + router.use(helloWorld) + + router.on('layerstart', function (req, res, layer) { + assert.equal('true', req.headers['x-layer']) + assert.equal(200, res.statusCode) + assert(Layer.prototype.isPrototypeOf(layer)) + done() + }) + + request(server).get('/').set({'x-layer': 'true'}).end() + }) + + it('should be emitted for each layer of the router stack', function (done) { + var router = new Router() + var server = createServer(router) + + var handlers = [passThrough, passThrough, passThrough] + router.use(handlers) + var start = 0 + var end = 0 + + router.on('layerstart', function () { + start++ + }) + router.on('layerend', function () { + end++ + if (end === handlers.length) { + assert.equal(start, end) + done() + } + }) + + request(server).get('/').end() + }) + + it('should bubble events on mounted routers', function () { + var router1 = new Router() + var router2 = new Router() + var server = createServer(router1) + router2.use(passThrough, passThrough) + router1.use(router2) + var start1 = 0 + var start2 = 0 + var end1 = 0 + var end2 = 0 + + router2.on('layerstart', function () { + start2++ + }) + router1.on('layerend', function () { + end2++ + }) + router1.on('layerstart', function () { + start1++ + }) + router1.on('layerend', function () { + end1++ + if (end1 === 3) { + assert.equal(start1, 3) + assert.equal(start2, 2) + assert.equal(end2, 2) + done() + } + }) + + request(server).get('/').end() + }) + }) + }) }) +function passThrough(req, res, next) { + next() +} + function helloWorld(req, res) { res.statusCode = 200 res.setHeader('Content-Type', 'text/plain')