diff --git a/lib/stoppable.js b/lib/stoppable.js index 68d526c..d288d14 100644 --- a/lib/stoppable.js +++ b/lib/stoppable.js @@ -16,6 +16,7 @@ module.exports = (server, grace) => { server.on('request', onRequest) server.stop = stop + server.stopAsync = stopAsync server._pendingSockets = reqsPerSocket return server @@ -51,6 +52,17 @@ module.exports = (server, grace) => { }) } + function stopAsync () { + return new Promise((resolve, reject) => { + server.stop((err, gracefully) => { + if (err) { + return reject(err) + } + resolve(gracefully) + }) + }) + } + function endIfIdle (requests, socket) { if (requests === 0) socket.end() } diff --git a/readme.md b/readme.md index d6f1d62..0787a71 100644 --- a/readme.md +++ b/readme.md @@ -56,6 +56,16 @@ Closes the server. - callback: passed along to the existing `server.close` function to auto-register a 'close' event. The first agrument is an error, and the second argument is a boolean that indicates whether it stopped gracefully. +**stopAsync()** + +```js +await server.stopAsync() +``` + +Closes the server, `Promise`-style. + +Resolves with a boolean that indicates whether it stopped gracefully. + ## Design decisions - Monkey patching generally sucks, but in this case it's the nicest API. Let's call it "decorating." diff --git a/test/stoppable.test.js b/test/stoppable.test.js index e35b04a..f9a3113 100644 --- a/test/stoppable.test.js +++ b/test/stoppable.test.js @@ -115,6 +115,13 @@ Object.keys(schemes).forEach(schemeName => { it('gracefully', () => { assert.isOk(gracefully) }) + + it('should error if already closed', function (done) { + server.stop(e => { + assert.propertyVal(e, 'code', 'ERR_SERVER_NOT_RUNNING') + done() + }) + }) }) describe('with keep-alive connections', () => { @@ -155,6 +162,100 @@ Object.keys(schemes).forEach(schemeName => { server.listen(PORT) server.stop(done) }) + + it('should error if already closed', function (done) { + server.stop(e => { + assert.propertyVal(e, 'code', 'ERR_SERVER_NOT_RUNNING') + done() + }) + }) + }) + }) + + describe('.stopAsync()', function () { + describe('without keep-alive connections', function () { + let gracefully = false + let server + + beforeEach(async function () { + server = stoppable(scheme.server()) + server.listen(PORT) + await a.event(server, 'listening') + const res1 = + await request(`${schemeName}://localhost:${PORT}`).agent(scheme.agent()) + const text1 = await res1.text() + assert.equal(text1, 'hello') + gracefully = (await Promise.all([ + server.stopAsync(), + a.event(server, 'close') + ])) + .shift() + }) + + it('stops accepting new connections', async () => { + const err = await a.failure( + request(`${schemeName}://localhost:${PORT}`).agent(scheme.agent())) + assert.match(err.message, /ECONNREFUSED/) + }) + + it('gracefully', () => { + assert.isOk(gracefully) + }) + + it('should reject if already closed', async function () { + const err = await a.failure(server.stopAsync()) + assert.propertyVal(err, 'code', 'ERR_SERVER_NOT_RUNNING') + }) + }) + + describe('with keep-alive connections', () => { + let gracefully = false + let server + let result + + beforeEach(async function () { + server = stoppable(scheme.server()) + server.listen(PORT) + await a.event(server, 'listening') + result = await request(`${schemeName}://localhost:${PORT}`) + .agent(scheme.agent({keepAlive: true})) + gracefully = (await Promise.all([ + server.stopAsync(), + a.event(server, 'close') + ])) + .shift() + }) + + it('returns the correct response', async function () { + const text1 = await result.text() + assert.equal(text1, 'hello') + }) + + it('stops accepting new connections', async () => { + const err = await a.failure(request(`${schemeName}://localhost:${PORT}`) + .agent(scheme.agent({ keepAlive: true }))) + assert.match(err.message, /ECONNREFUSED/) + }) + + it('gracefully', () => { + assert.isOk(gracefully) + }) + + it('empties all sockets once closed', async () => { + await a.failure(request(`${schemeName}://localhost:${PORT}`) + .agent(scheme.agent({ keepAlive: true }))) + assert.equal(server._pendingSockets.size, 0) + }) + + it('registers the "close" callback', async () => { + server.listen(PORT) + await server.stopAsync() + }) + + it('should reject if already closed', async function () { + const err = await a.failure(server.stopAsync()) + assert.propertyVal(err, 'code', 'ERR_SERVER_NOT_RUNNING') + }) }) })