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

Close queue #16

Merged
merged 9 commits into from
Jul 10, 2017
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
language: node_js

node_js:
- "4"
- "8"
- "7"
- "6"
- "5"
- "4"

notifications:
email:
on_success: never
on_failure: always
77 changes: 76 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ function third (instance, opts, cb) {
* <a href="#after"><code>instance.<b>after()</b></code></a>
* <a href="#ready"><code>instance.<b>ready()</b></code></a>
* <a href="#override"><code>instance.<b>override()</b></code></a>
* <a href="#onClose"><code>instance.<b>onClose()</b></code></a>
* <a href="#close"><code>instance.<b>close()</b></code></a>
* <a href="#express"><code>avvio.<b>express()</b></code></a>

-------------------------------------------------------
Expand Down Expand Up @@ -224,7 +226,7 @@ Returns the instance on which `after` is called, to support a chainable API.

### app.ready(func(error, [context], [done]))

Calls a functon after all the plugins and `after` call are completed, but before `'start'` is emitted. `ready` callbacks are executed one at a time.
Calls a function after all the plugins and `after` call are completed, but before `'start'` is emitted. `ready` callbacks are executed one at a time.

The callback changes basing on the parameters your are giving:
1. If one parameter is given to the callback, that parameter will be the `error` object.
Expand Down Expand Up @@ -316,6 +318,79 @@ app.use(function first (s1, opts, cb) {
```
-------------------------------------------------------

<a name="onClose"></a>
### app.onClose(func(error, [context], [done]))

Registers a new callback that will be fired once then `close` api is called.

The callback changes basing on the parameters your are giving:
1. If one parameter is given to the callback, that parameter will be the `error` object.
2. If two parameters are given to the callback, the first will be the `error` object, the second will be the `done` callback.
3. If three parameters are given to the callback, the first will be the `error` object, the second will be the `context` and the third the `done` callback.

```js
const server = {}
...
// onClose with one parameter
boot.onClose(function (err) {
if (err) throw err
})

// onClose with two parameter
boot.onClose(function (err, done) {
if (err) throw err
done()
})

// onClose with three parameters
boot.onClose(function (err, context, done) {
if (err) throw err
assert.equal(context, server)
done()
})
```

`done` must be called only once.
Returns the instance on which `onClose` is called, to support a chainable API.

-------------------------------------------------------

<a name="close"></a>
### app.close(func(error, [context], [done]))

Starts the shotdown procedure, the callback is called once all the registered callbacks with `onClose` has been executed.

The callback changes basing on the parameters your are giving:
1. If one parameter is given to the callback, that parameter will be the `error` object.
2. If two parameters are given to the callback, the first will be the `error` object, the second will be the `done` callback.
3. If three parameters are given to the callback, the first will be the `error` object, the second will be the `context` and the third the `done` callback.

```js
const server = {}
...
// close with one parameter
boot.close(function (err) {
if (err) throw err
})

// close with two parameter
boot.close(function (err, done) {
if (err) throw err
done()
})

// close with three parameters
boot.close(function (err, context, done) {
if (err) throw err
assert.equal(context, server)
done()
})
```

`done` must be called only once.

-------------------------------------------------------

## Acknowledgements

This project was kindly sponsored by [nearForm](http://nearform.com).
Expand Down
75 changes: 19 additions & 56 deletions boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const fastq = require('fastq')
const EE = require('events').EventEmitter
const inherits = require('util').inherits
const Plugin = require('./plugin')

function wrap (server, opts, instance) {
const expose = opts.expose || {}
Expand Down Expand Up @@ -77,6 +78,11 @@ function Boot (server, opts, done) {
this._readyQ.drain = () => {
this.emit('start')
}
this._closeQ = fastq(this, callWithCbOrNextTick, 1)
this._closeQ.pause()
this._closeQ.drain = () => {
this.emit('close')
}

// we init, because we need to emit "start" if no use is called
this._init()
Expand All @@ -92,7 +98,7 @@ Boot.prototype._init = function () {
// we need to wait any call to use() to happen
process.nextTick(done)
}, {}, noop)
loadPlugin.call(this, main, (err) => {
Plugin.loadPlugin.call(this, main, (err) => {
if (err) {
this._error = err
if (this._readyQ.length() === 0) {
Expand Down Expand Up @@ -164,68 +170,25 @@ Boot.prototype.after = function (func, cb) {
return this
}

Boot.prototype.ready = function (func) {
this._readyQ.push(func)
Boot.prototype.onClose = function (func) {
this._closeQ.push(func, err => {
if (err) this._error = err
})
Copy link
Member

Choose a reason for hiding this comment

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

I think we should call unshift() here. More or less, the latest you register a shutdown part, the sooner it will be executed. This is the inverse of the start phase.

return this
}

function noop () {}

function Plugin (parent, func, opts, callback) {
this.func = func
this.opts = opts
this.callback = callback
this.deferred = false
this.onFinish = null
this.parent = parent

this.q = fastq(parent, loadPlugin, 1)
this.q.pause()

// always start the queue in the next tick
// because we try to attach subsequent call to use()
// to the right plugin. we need to defer them,
// or they will end up at the top of _current
process.nextTick(this.q.resume.bind(this.q))
}

Plugin.prototype.exec = function (server, cb) {
const func = this.func
this.server = this.parent.override(server, func, this.opts)
func(this.server, this.opts, cb)
Boot.prototype.close = function (cb) {
this._error = null
this._closeQ.push(cb)
process.nextTick(this._closeQ.resume.bind(this._closeQ))
}

Plugin.prototype.finish = function (err, cb) {
const callback = this.callback
// if 'use' has a callback
if (callback) {
callback(err)
// if 'use' has a callback but does not have parameters
cb(callback.length > 0 ? null : err)
} else {
cb(err)
}
Boot.prototype.ready = function (func) {
this._readyQ.push(func)
return this
}

// loads a plugin
function loadPlugin (toLoad, cb) {
const last = this._current[0]
// place the plugin at the top of _current
this._current.unshift(toLoad)
toLoad.exec((last && last.server) || this._server, (err) => {
if (err || !(toLoad.q.length() > 0 || toLoad.q.running() > 0)) {
// finish now, because there is nothing left to do
this._current.shift()
toLoad.finish(err, cb)
} else {
// finish when the queue of nested plugins to load is empty
toLoad.q.drain = () => {
this._current.shift()
toLoad.finish(null, cb)
}
}
})
}
function noop () {}

function callWithCbOrNextTick (func, cb, context) {
if (this && this._server) {
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@
},
"homepage": "https://github.com/mcollina/avvio#readme",
"devDependencies": {
"express": "^4.14.0",
"pre-commit": "^1.1.3",
"standard": "^10.0.0",
"tap": "^10.0.0"
"express": "^4.15.3",
"pre-commit": "^1.2.2",
"standard": "^10.0.2",
"tap": "^10.7.0"
},
"dependencies": {
"fastq": "^1.4.1"
"fastq": "^1.5.0"
}
}
62 changes: 62 additions & 0 deletions plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict'

const fastq = require('fastq')

function Plugin (parent, func, opts, callback) {
this.func = func
this.opts = opts
this.callback = callback
this.deferred = false
this.onFinish = null
this.parent = parent

this.q = fastq(parent, loadPlugin, 1)
this.q.pause()

// always start the queue in the next tick
// because we try to attach subsequent call to use()
// to the right plugin. we need to defer them,
// or they will end up at the top of _current
process.nextTick(this.q.resume.bind(this.q))
}

Plugin.prototype.exec = function (server, cb) {
const func = this.func
this.server = this.parent.override(server, func, this.opts)
func(this.server, this.opts, cb)
}

Plugin.prototype.finish = function (err, cb) {
const callback = this.callback
// if 'use' has a callback
if (callback) {
callback(err)
// if 'use' has a callback but does not have parameters
cb(callback.length > 0 ? null : err)
} else {
cb(err)
}
}

// loads a plugin
function loadPlugin (toLoad, cb) {
const last = this._current[0]
// place the plugin at the top of _current
this._current.unshift(toLoad)
toLoad.exec((last && last.server) || this._server, (err) => {
if (err || !(toLoad.q.length() > 0 || toLoad.q.running() > 0)) {
// finish now, because there is nothing left to do
this._current.shift()
toLoad.finish(err, cb)
} else {
// finish when the queue of nested plugins to load is empty
toLoad.q.drain = () => {
this._current.shift()
toLoad.finish(null, cb)
}
}
})
}

module.exports = Plugin
module.exports.loadPlugin = loadPlugin
Loading