-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
emit more events #1395
base: master
Are you sure you want to change the base?
emit more events #1395
Changes from 14 commits
83940d0
4c4d064
82b5f1c
2faaa63
7303f20
c719bdb
0fc13d9
e4bf680
2d181dd
7190d4e
6b996ba
31911c4
4107093
4d70048
3b47e65
1bdfebb
0681713
c3369bf
bc62b2a
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 |
---|---|---|
|
@@ -219,6 +219,68 @@ Note: | |
- Many properties on `ctx` are defined using getters, setters, and `Object.defineProperty()`. You can only edit these properties (not recommended) by using `Object.defineProperty()` on `app.context`. See https://github.com/koajs/koa/issues/652. | ||
- Mounted apps currently use their parent's `ctx` and settings. Thus, mounted apps are really just groups of middleware. | ||
|
||
## Events | ||
|
||
Koa application is an instance of `EventEmitter` and emits following events to allow hooks into lifecycle. | ||
All these events (except `error`) have `ctx` as first parameter: | ||
|
||
### Event: 'request' | ||
|
||
Emitted each time there is a request, with `ctx` as parameter, before passing to any middleware. | ||
May be a good place for per-request context mutation, when needed, for example: | ||
|
||
```js | ||
app.on('request', ctx => { | ||
ctx.state.start = Date.now(); | ||
// or something more advanced | ||
if(!ctx.get('DNT')) ctx.state = new Proxy({}) | ||
}) | ||
``` | ||
|
||
### Event: 'respond' | ||
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. i don't see how this or the 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. it actually has less benefit because you cannot use async functions 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. There is no need to have advantage or disadvantage and not always sync call is inferior to async call from application point of view. If you follow Node.JS core development strategy then you certainly will notice “mix and match” innovation in handling async flow - |
||
|
||
Emitted after passing all middleware, but before sending the response to network. | ||
May be used when some action required to be latest after all middleware processing, for example: | ||
|
||
```js | ||
app.on('respond', ctx => { | ||
if (ctx.state.start) ctx.set('X-Response-Time', Date.now() - ctx.state.start) | ||
}) | ||
``` | ||
|
||
Note: `respond` event may not be emitted in case of premature socket close (due to a middleware timeout, for example). | ||
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. exceptions like this make it less useful and more difficult to maintain 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. It's not an exception - it's a logic of this event because the response in this case never happened. |
||
|
||
### Event: 'responded' | ||
|
||
Emitted when the response stream is finished. Good place to cleanup any resources attached to `ctx.state` for example: | ||
|
||
```js | ||
app.on('responded', ctx => { | ||
if (ctx.state.dataStream) ctx.state.dataStream.destroy(); | ||
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. i don't understand this use-case. clean up callbacks on streams should be handled when you create and pipe them. 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. How would you know from the stream that, for example, connection with a client was prematurely lost and not the whole stream was consumed? |
||
}) | ||
``` | ||
|
||
More advanced example, use events to detect that server is idling for some time: | ||
tinovyatkin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```js | ||
const server = app.listen(); | ||
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. i would actually recommend not doing this in koa. i would do this as a req/res decorator: const fn = app.callback()
const checkIdle = (fn) => (req, res) => {
// do stuff
return fn(req, res)
}
http.createServer(checkIdle(fn)) 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. I probably missing something here as I’m struggling to understand: if you created Koa as an extension of |
||
const IDLE_INTERVAL = 2 * server.timeout; | ||
const onIdle = () => { console.warn(`Server is idle for ${IDLE_INTERVAL / 1000} seconds!`); } | ||
let idleInterval = setInterval(onIdle, IDLE_INTERVAL); | ||
app | ||
.on('request', () => { | ||
clearInterval(idleInterval); | ||
}) | ||
.on('responded', () => { | ||
clearInterval(idleInterval); | ||
idleInterval = setInterval(onIdle, IDLE_INTERVAL); | ||
}) | ||
``` | ||
|
||
### Event: 'error' | ||
|
||
See **Error Handling** below. | ||
|
||
## Error Handling | ||
|
||
By default outputs all errors to stderr unless `app.silent` is `true`. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -156,9 +156,23 @@ module.exports = class Application extends Emitter { | |
handleRequest(ctx, fnMiddleware) { | ||
const res = ctx.res; | ||
res.statusCode = 404; | ||
const onerror = err => ctx.onerror(err); | ||
const handleResponse = () => respond(ctx); | ||
|
||
const responding = Symbol('responding'); | ||
this.once(responding, () => this.emit('respond', ctx)); | ||
|
||
const onerror = err => { | ||
if (null != err) { | ||
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. the old code will execute 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. @fengmk2 |
||
ctx.onerror(err); | ||
this.emit(responding); | ||
} | ||
}; | ||
const handleResponse = () => { | ||
this.emit(responding); | ||
respond(ctx); | ||
}; | ||
this.emit('request', ctx); | ||
onFinished(res, onerror); | ||
onFinished(ctx.socket, () => { this.emit('responded', ctx); }); | ||
return fnMiddleware(ctx).then(handleResponse).catch(onerror); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
|
||
'use strict'; | ||
|
||
const assert = require('assert'); | ||
const Koa = require('../..'); | ||
const request = require('supertest'); | ||
|
||
describe('app emits events', () => { | ||
it('should emit request, respond and responded once and in correct order', async() => { | ||
const app = new Koa(); | ||
const emitted = []; | ||
['request', 'respond', 'responded', 'error', 'customEvent'].forEach(event => app.on(event, () => emitted.push(event))); | ||
|
||
app.use((ctx, next) => { | ||
emitted.push('fistMiddleWare'); | ||
ctx.body = 'hello!'; | ||
return next(); | ||
}); | ||
|
||
app.use((ctx, next) => { | ||
ctx.app.emit('customEvent'); | ||
return next(); | ||
}); | ||
|
||
app.use(ctx => { | ||
emitted.push('lastMiddleware'); | ||
}); | ||
|
||
const server = app.listen(); | ||
|
||
let onceEvents = 0; | ||
['request', 'respond', 'responded'].forEach(event => | ||
app.once(event, ctx => { | ||
assert.strictEqual(ctx.app, app); | ||
onceEvents++; | ||
}) | ||
); | ||
|
||
await request(server) | ||
.get('/') | ||
.expect(200); | ||
|
||
assert.deepStrictEqual(emitted, ['request', 'fistMiddleWare', 'customEvent', 'lastMiddleware', 'respond', 'responded']); | ||
assert.strictEqual(onceEvents, 3); | ||
}); | ||
|
||
it('should emit error event on middleware throw', async() => { | ||
const app = new Koa(); | ||
const emitted = []; | ||
['request', 'respond', 'responded', 'error'].forEach(event => app.on(event, () => emitted.push(event))); | ||
|
||
app.use((ctx, next) => { | ||
throw new TypeError('Hello Koa!'); | ||
}); | ||
|
||
const server = app.listen(); | ||
|
||
let onceEvents = 0; | ||
app.once('error', (err, ctx) => { | ||
assert.ok(err instanceof TypeError); | ||
assert.strictEqual(ctx.app, app); | ||
onceEvents++; | ||
}); | ||
|
||
await request(server) | ||
.get('/') | ||
.expect(500); | ||
|
||
assert.deepStrictEqual(emitted, ['request', 'error', 'respond', 'responded']); | ||
assert.strictEqual(onceEvents, 1); | ||
}); | ||
|
||
it('should emit correct events on middleware timeout', async() => { | ||
const app = new Koa(); | ||
const emitted = []; | ||
['request', 'respond', 'responded', 'error', 'timeout'].forEach(event => app.on(event, () => emitted.push(event))); | ||
|
||
app.use(async(ctx, next) => { | ||
await new Promise(resolve => setTimeout(resolve, 2000)); | ||
ctx.body = 'Timeout'; | ||
}); | ||
|
||
const server = app.listen(); | ||
server.setTimeout(1000, socket => { app.emit('timeout'); socket.end('HTTP/1.1 408 Request Timeout\r\n\r\n'); }); | ||
|
||
await request(server) | ||
.get('/') | ||
.expect(408); | ||
|
||
assert.deepStrictEqual(emitted, ['request', 'responded', 'timeout']); | ||
}); | ||
}); |
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.
thanks for this doc, very useful for a PR