Comparing version 0.2.1 to 0.3.1
@@ -14,5 +14,5 @@ 'use strict' | ||
exports = module.exports = require('./lib/eva-application') | ||
exports.Client = Eva | ||
exports = module.exports = Eva | ||
exports.Application = require('./lib/eva-application') | ||
exports.Promise = require('bluebird') | ||
exports.version = require('./lib/version').number |
'use strict' | ||
var urlParse = require('url').parse | ||
var WebSocket = require('ws') | ||
var EventEmitter = require('events') | ||
var NetServer = require('net').Server | ||
var http = require('http') | ||
var WebSocketServer = require('ws').Server | ||
var Eva = require('./eva') | ||
var version = require('./version') | ||
function EvaApplication(server, path, handler, verifyClient) { | ||
if (!(this instanceof EvaApplication)) {return new EvaApplication(server, path, handler, verifyClient)} | ||
if (typeof path === 'function') { | ||
verifyClient = handler | ||
handler = path | ||
function EvaApplication(server, path, options) { | ||
if (!(this instanceof EvaApplication)) { | ||
return new EvaApplication(server, path, options) | ||
} | ||
if (typeof server === 'number' && isFinite(server)) { | ||
this.server = server = http.createServer(function (req, res) { | ||
var body = http.STATUS_CODES[426]; | ||
res.writeHead(426, { | ||
'Content-Length': body.length, | ||
'Content-Type': 'text/plain' | ||
}); | ||
res.end(body); | ||
}).listen(~~server) | ||
} | ||
if (!(server instanceof NetServer)) { | ||
throw TypeError('Invalid argument. A server object must be provided.') | ||
} | ||
// Allow options as second argument. | ||
if (typeof path === 'object' && path !== null) { | ||
options = path | ||
path = '/' | ||
} | ||
// Default arguments. | ||
path = path ? String(path) : '/' | ||
if (path[0] !== '/') {path = '/' + path} | ||
options = options || {} | ||
// Inherit from EventEmitter. | ||
EventEmitter.call(this) | ||
// Private variables. | ||
var self = this | ||
var useHeartbeat = options.useHeartbeat == null ? true : !!options.useHeartbeat | ||
this._locked = false | ||
this._originalHandleUpgrade = undefined | ||
this._clients = [] | ||
this._handlers = typeof handler === 'function' ? handler : null | ||
this._multipleHandlers = false | ||
var onClose = function () { | ||
var index = self._clients.indexOf(this) | ||
index === -1 || self._clients.splice(index, 1) | ||
} | ||
var handle = function (handler) {handler(this)} | ||
this._wss = new WebSocket.Server({ | ||
// WebSocket Server attachment. | ||
this._wss = new WebSocketServer({ | ||
server: server, | ||
@@ -35,3 +57,3 @@ path: path, | ||
clientTracking: false, | ||
verifyClient: verifyClient | ||
verifyClient: options.verifyClient | ||
}) | ||
@@ -41,12 +63,26 @@ this._wss.on('connection', function (ws) { | ||
self._clients.push(client) | ||
client.on('killed', onClose) | ||
if (self._multipleHandlers) { | ||
self._handlers.forEach(handle, client) | ||
} else if (self._handlers) { | ||
;(0, self._handlers)(client) // Dereference `self`. | ||
client.on('killed', onClientKilled) | ||
if (useHeartbeat) { | ||
client._heartbeatTimer = setInterval(newHeartbeatTimer(client), 55000) | ||
} | ||
self.emit('client', client) | ||
}) | ||
this._rejectFuture = function () { | ||
var original = self._wss.handleUpgrade | ||
self._wss.handleUpgrade = function (req, socket) { | ||
// Client tracking. | ||
function onClientKilled() { | ||
var index = self._clients.indexOf(this) | ||
if (index >= 0) { | ||
self._clients.splice(index, 1) | ||
if (self._locked && self._clients.length === 0) { | ||
self.emit('vacant') | ||
} | ||
} | ||
} | ||
} | ||
EvaApplication.prototype = { | ||
lock: function () { | ||
if (this._locked) {return} | ||
this._locked = true | ||
var original = this._originalHandleUpgrade = this._wss.handleUpgrade | ||
this._wss.handleUpgrade = function (req, socket) { | ||
var u = urlParse(req.url) | ||
@@ -59,30 +95,12 @@ if (u && u.pathname === path) { | ||
} | ||
self._rejectFuture = function () {} | ||
} | ||
} | ||
EvaApplication.prototype = { | ||
addHandler: function (handler) { | ||
if (this._multipleHandlers) { | ||
this._handlers.push(handler) | ||
} else if (this._handlers) { | ||
this._multipleHandlers = true | ||
this._handlers = [this._handlers, handler] | ||
} else { | ||
this._handlers = handler | ||
if (this._clients.length === 0) { | ||
this.emit('vacant') | ||
} | ||
return this | ||
}, | ||
removeHandler: function (handler) { | ||
if (this._multipleHandlers) { | ||
var index = this._handlers.indexOf(handler) | ||
if (index >= 0) { | ||
this._handlers.splice(index, 1) | ||
if (this._handlers.length < 2) { | ||
this._multipleHandlers = false | ||
this._handlers = this._handlers[0] | ||
} | ||
} | ||
} else if (this._handlers === handler) { | ||
this._handlers = null | ||
} | ||
unlock: function () { | ||
if (!this._locked) {return} | ||
this._locked = false | ||
this._wss.handleUpgrade = this._originalHandleUpgrade | ||
this._originalHandleUpgrade = undefined | ||
return this | ||
@@ -94,3 +112,3 @@ }, | ||
destroy: function () { | ||
this._rejectFuture() | ||
this.lock() | ||
forEach(this._clients, Eva.prototype.destroy) | ||
@@ -100,3 +118,3 @@ return this | ||
kill: function () { | ||
this._rejectFuture() | ||
this.lock() | ||
forEach(this._clients, Eva.prototype.kill) | ||
@@ -106,3 +124,3 @@ return this | ||
done: function () { | ||
this._rejectFuture() | ||
this.lock() | ||
forEach(this._clients, Eva.prototype.done) | ||
@@ -114,7 +132,11 @@ return this | ||
for (var i=0, len=clients.length; i<len; i++) { | ||
clients[i].isReady && clients[i].send(eventName, data) | ||
clients[i].isReady && clients[i].emit(eventName, data) | ||
} | ||
return this | ||
}, | ||
get isLocked () { | ||
return this._locked | ||
} | ||
} | ||
require('util').inherits(EvaApplication, EventEmitter) | ||
module.exports = EvaApplication | ||
@@ -128,2 +150,18 @@ | ||
function newHeartbeatTimer(client) { | ||
client._ws.on('pong', function () { | ||
client._pendingPings = 0 | ||
}) | ||
return function () { | ||
if (client._pendingPings++ >= 10) { | ||
if (!client._fatalError) { | ||
client._fatalError = new Error('Remote endpoint failed to reciprocate a heartbeat after 10 tries.') | ||
} | ||
client.destroy() | ||
} else { | ||
client._ws.ping() | ||
} | ||
} | ||
} | ||
// Better performance than array.slice(). | ||
@@ -130,0 +168,0 @@ function copy(arr) { |
@@ -9,3 +9,2 @@ 'use strict' | ||
var isBrowser = require('./platform').isBrowser | ||
var PASSTHROUGH = function (x) {return x} | ||
var STATES = { | ||
@@ -40,2 +39,4 @@ CONNECTING: 0, | ||
this._finalBufferedAmount = 0 | ||
this._heartbeatTimer = null | ||
this._pendingPings = 0 | ||
@@ -55,3 +56,9 @@ // Make sure Eva doesn't receive Blobs. | ||
if (!isBrowser) { | ||
catchCloseFrame(this._ws._receiver, onclose) | ||
if (ws.readyState === ws.OPEN) { | ||
catchCloseFrame(self._ws._receiver, onclose) | ||
} else { | ||
ws.on('open', function () { | ||
catchCloseFrame(self._ws._receiver, onclose) | ||
}) | ||
} | ||
} | ||
@@ -76,3 +83,3 @@ | ||
if (message.flags.isResponse) { | ||
fulfill(self, message.data, message.UID) | ||
fulfill(self, message.data, message.UID, false) | ||
self._partnerIsConcerned && checkOurHappiness(self) | ||
@@ -83,7 +90,12 @@ } else if (message.UID === 0xffffffff) { | ||
checkOurHappiness(self) | ||
} else if (message.UID > 0) { | ||
emitFirst(self._external, message.eventName, message.data, message.UID) | ||
} else { | ||
self._external.emit(message.eventName, message.data, message.UID) | ||
self._external.emit(message.eventName, message.data, 0) | ||
} | ||
}) | ||
// Protect against unsolicited `error` events. | ||
this._external.on('error', function () {}) | ||
// Indicate when Eva is ready. | ||
@@ -112,4 +124,8 @@ if (ws.readyState === ws.OPEN) { | ||
this._state = STATES.DONE | ||
if (this._heartbeatTimer) { | ||
clearInterval(this._heartbeatTimer) | ||
this._heartbeatTimer = null | ||
} | ||
if (this._fatalError) { | ||
this.on('error', function () {}) | ||
this._internal.on('error', function () {}) | ||
this._internal.emit('error', this._fatalError) | ||
@@ -157,3 +173,3 @@ } | ||
}, | ||
emit: function (eventName, data) { | ||
run: function (eventName, data) { | ||
eventName = validateNewEvent(this, eventName) | ||
@@ -174,3 +190,3 @@ var UID = this._nextUID | ||
}, | ||
send: function (eventName, data) { | ||
emit: function (eventName, data) { | ||
eventName = validateNewEvent(this, eventName) | ||
@@ -181,3 +197,3 @@ this._ws.send(BeepBoop.write(eventName, data, 0)) | ||
on: function (eventName, listener) { | ||
return delegateEventListener(this, eventName, listener) | ||
return delegateEventListener(this, eventName, listener, false) | ||
}, | ||
@@ -188,10 +204,8 @@ once: function (eventName, listener) { | ||
removeListener: function (eventName, listener) { | ||
return delegateListenerRemoval(this, false, eventName, listener) | ||
}, | ||
removeAllListeners: function (eventName) { | ||
if (arguments.length > 0) { | ||
return delegateListenerRemoval(this, true, eventName) | ||
eventName = BeepBoop.getEventName(eventName) | ||
if (isForbiddenEvent(eventName)) { | ||
throw new SyntaxError('Eva does not support the "' + eventName + '" event.') | ||
} | ||
this._internal && this._internal.removeAllListeners() | ||
this._external && this._external.removeAllListeners() | ||
var emitter = isInternalEvent(eventName) ? this._internal : this._external | ||
emitter && emitter.removeListener(eventName, listener) | ||
return this | ||
@@ -232,6 +246,6 @@ }, | ||
function isInternalEvent(eventName) { | ||
return eventName === 'done' | ||
return eventName === 'killed' | ||
|| eventName === 'ready' | ||
|| eventName === 'error' | ||
|| eventName === 'killed' | ||
|| eventName === 'done' | ||
} | ||
@@ -268,3 +282,3 @@ function isForbiddenEvent(eventName) { | ||
isPromise(result) | ||
? result.catch(PASSTHROUGH).then(function (data) {respondToEvent(eva, data, UID)}) | ||
? result.catch(whenErrorResponse).then(function (data) {respondToEvent(eva, data, UID)}) | ||
: respondToEvent(eva, result, UID) | ||
@@ -274,13 +288,11 @@ } | ||
} | ||
function delegateListenerRemoval(eva, all, eventName, listener) { | ||
eventName = BeepBoop.getEventName(eventName) | ||
if (isForbiddenEvent) { | ||
throw new SyntaxError('Eva does not support the "' + eventName + '" event.') | ||
function emitFirst(emitter, eventName, a, b) { | ||
var handler = emitter._events && emitter._events[eventName] | ||
if (!handler) {return} | ||
if (typeof handler === 'function') { | ||
handler.call(emitter, a, b) | ||
} else { | ||
handler[0] && handler[0].call(emitter, a, b) | ||
} | ||
var emitter = isInternalEvent(eventName) ? eva._internal : eva._external | ||
if (emitter) { | ||
all ? emitter.removeAllListeners(eventName) | ||
: emitter.removeListener(eventName, listener) | ||
} | ||
return eva | ||
} | ||
@@ -321,3 +333,5 @@ | ||
eva._pendings.delete(UID) | ||
supress && fate.promise.suppressUnhandledRejections() | ||
if (supress) { | ||
fate.promise.suppressUnhandledRejections() | ||
} | ||
;(response instanceof Error ? fate.reject : fate.resolve)(response) | ||
@@ -354,2 +368,10 @@ } | ||
function whenErrorResponse(err) { | ||
if (err instanceof Error) { | ||
err = new Error(err.message) | ||
err.fileName = null | ||
} | ||
return err | ||
} | ||
function fatalError(eva, err, statusCode) { | ||
@@ -372,3 +394,6 @@ if (!eva._fatalError && !eva._statusCode) { | ||
function releaseResources(eva) { | ||
eva._killTimer && clearTimeout(eva._killTimer) | ||
if (eva._killTimer) { | ||
clearTimeout(eva._killTimer) | ||
eva._killTimer = null | ||
} | ||
eva._internal.emit('killed', eva._selfIsHappy && eva._partnerIsHappy) | ||
@@ -375,0 +400,0 @@ eva._finalBufferedAmount = eva._ws.bufferedAmount |
{ | ||
"name": "eva-events", | ||
"version": "0.2.1", | ||
"version": "0.3.1", | ||
"description": "Eva is like an EventEmitter, but over WebSockets.", | ||
@@ -28,3 +28,3 @@ "main": "index.js", | ||
"dependencies": { | ||
"bluebird": "^3.1.1", | ||
"bluebird": "^3.1.2", | ||
"is-promise": "^2.1.0", | ||
@@ -31,0 +31,0 @@ "msgpack-lite-lite": "^0.4.0", |
104
README.md
@@ -17,10 +17,8 @@ ![Image of Eve](http://s11.postimg.org/h1cc0m88z/eveflying.jpg) | ||
```javascript | ||
var Eva = require('eva-events') | ||
var Eva = require('eva-events'); | ||
var eva = new Eva('ws://myapp.com'); | ||
var eva = new Eva('ws://myapp.com') | ||
eva.on('hello world', function (msg) { | ||
console.log(msg) // "Hello world!" | ||
return new Date | ||
}) | ||
eva.on('greeting', function (msg) { | ||
console.log(msg); // "Hello world!"; | ||
}); | ||
``` | ||
@@ -30,11 +28,8 @@ | ||
```javascript | ||
var Eva = require('eva-events') | ||
var EvaApp = require('eva-events').Application; | ||
var app = EvaApp(server); | ||
var app = Eva(server, function (eva) { | ||
eva.emit('hello world', 'Hello world!') | ||
.then(function (reply) { | ||
reply instanceof Date // true | ||
this === eva // true | ||
}) | ||
}) | ||
app.on('client', function (eva) { | ||
eva.emit('greeting', 'Hello world!'); | ||
}); | ||
``` | ||
@@ -45,3 +40,3 @@ | ||
The `.emit()` method returns a promise which is resolved with the return value of the remote event listener. If an `Error` object is returned, the promise is rejected with that error. | ||
Aside, from the [`.emit()`](#emitstring-eventname-any-data---this) method, you can also use the [`.run()`](#runstring-eventname-any-data---promise) method, which returns a promise which is fulfilled with the return value of the remote event listener. If an `Error` object is returned, the promise is rejected with that error. | ||
@@ -66,12 +61,6 @@ ## Special reserved events | ||
## Bad practices | ||
## Caution when using `.run()` | ||
Avoid these things when using `eva-events`: | ||
If you [`.run()`](#runstring-eventname-any-data---promise) an event before the other endpoint has registered a listener for that event, [`eva`](#class-eva) will wait for a response indefinitely. Other things can cause transactions to wait indefinitely too, such as poorly written application code. To prevent this, you can use [`.timeout()`](#timeoutnumber-sec---this) which causes all new [`.run()`](#runstring-eventname-any-data---promise) transactions to fail and kill the connection if they aren't settled after a certain amount of time. You should always listen on the `killed` event to react to these things accordingly. | ||
#### Hanging transactions | ||
If you `.emit()` an event before the other endpoint has registered a listener for that event, [`eva`](#class-eva) will wait for a response indefinitely. Other things can cause transactions to wait indefinitely too, such as poorly written application code. To prevent this, you can use [`.timeout()`](#timeoutnumber-sec---this) which causes all new transactions to fail and kill the connection if they aren't resolved after a certain amount of time. You should always listen on the `killed` event to react to these things accordingly. | ||
#### Multiple listeners on the same event | ||
If you have multiple listeners for the same event, the returned promise will only see the value returned by the first listener. In other words, each `.emit()` can only have one response. [`eva`](#class-eva) does not prevent you from having multiple listeners on the same event because that might sometimes be useful for applications making use of [`.send()`](#sendstring-eventname-any-data---this). | ||
## Browser compatibility | ||
@@ -107,3 +96,3 @@ | ||
On the server, these instances are exposed in the application handler (see [EvaApplication](#class-evaapplication)). | ||
On the server, these instances are exposed by [EvaApplication](#class-evaapplication) in the `client` event. | ||
@@ -114,16 +103,14 @@ #### .kill() -> this | ||
#### .done() -> this | ||
Starts to gracefully end the connection. The `done` event is immediately emitted. Both endpoints will then wait for all pending transactions to be resolved before finally closing the underlying connection. | ||
Starts to gracefully end the connection. The `done` event is immediately emitted. Both endpoints will then wait for all pending transactions to be resolved before finally closing the underlying connection (at which point, the `killed` event is emitted). | ||
#### .emit(string *eventName*, [any *data*]) -> Promise | ||
Starts a transaction by emitting an event to the opposite endpoint. That endpoint's listeners will be invoked with *data* as the first argument. The promise returned by this method is resolved when it receives a response back from a listener. If the response is an `Error` object, the promise is rejected with it, otherwise the promise is fulfilled with whatever data was sent back. Promises returned by this method, and promises chained from that promise, have a `this` value of the [`eva`](#class-eva) instance. | ||
#### .emit(string *eventName*, [any *data*]) -> this | ||
This emits an event to the opposite endpoint. Remote event listeners are invoked with *data* as the first argument. | ||
#### .send(string *eventName*, [any *data*]) -> this | ||
This is the same as the [`.emit()`](#emitstring-eventname-any-data---promise) method, except that it does not expect back a response. Anything returned by the event listener is discarded. | ||
#### .run(string *eventName*, [any *data*]) -> Promise | ||
This is the same as [`.emit()`](#emitstring-eventname-any-data---this), except instead of just emitting an event, it starts a **transaction**. Only the first event listener for the event will fired, and that listener's return value is **sent back** and made available as the fulfillment value of the promise returned by this method. Or, if the return value is an `Error` object, the promise is rejected with it. Promises returned by this method, and promises chained from that promise, have a `this` value of the [`eva`](#class-eva) instance. Event listeners can return promises, whose fulfillment values or rejection reasons will be made available as the fulfillment value or rejection reason of the promise returned by this method. | ||
#### .on(string *eventName*, function *listener*) -> this | ||
#### .addListener(string *eventName*, function *listener*) -> this | ||
Registers an event listener *listener* for event *eventName*. | ||
#### .addListener(string *eventName*, function *listener*) -> this | ||
Alias for [`.on()`](#onstring-eventname-function-listener---this). | ||
#### .once(string *eventName*, function *listener*) -> this | ||
@@ -135,5 +122,2 @@ Same as [`.on()`](#onstring-eventname-function-listener---this), but the *listener* will only be invoked once. | ||
#### .removeAllListeners([string *eventName*]) -> this | ||
Unregisters all event listeners on the instance, or all listeners of event *eventName*. | ||
#### .listenerCount(string *eventName*) -> number | ||
@@ -143,3 +127,3 @@ Returns the number of event listeners that are registered with event *eventName*. | ||
#### .timeout(number *sec*) -> this | ||
Causes all future transactions to timeout after *sec* seconds, at which point an `error` event will be emitted and the connection will be forcefully killed. A *sec* value of `0` or `Infinity` turns off timeouts. | ||
Causes all future transactions (started by the [`.run()`](#runstring-eventname-any-data---promise) method) to timeout after *sec* seconds, at which point an `error` event will be emitted and the connection will be forcefully killed. A *sec* value of `0` or `Infinity` turns off timeouts. | ||
@@ -161,15 +145,20 @@ Default: 0 | ||
On the server: | ||
On the server only: | ||
```javascript | ||
var Eva = require('eva-events') | ||
var app = Eva(server, '/path/to/app', function (eva) { | ||
eva.emit('welcome', 'Welcome to my app.') | ||
}) | ||
var EvaApp = require('eva-events').Application | ||
var app = EvaApp(server, '/path/to/app', options) | ||
``` | ||
#### constructor EvaApplication(http.Server *server*, [string *path*], [function *handler*, [function *verifyClients*]]) | ||
Also supports `https` (thus, `wss://`) servers. | ||
#### constructor EvaApplication(http.Server *server*, [string *path*], [Object *options*]) | ||
You may supply a *verifyClients* function which must return `true` for each client wishing to connect to the WebSocket server. The function may have the following two signatures: | ||
*path* defaults to `"/"` | ||
[`EvaApplication`](#class-evaapplication) is an `EventEmitter`. When a new client connects, the `client` event is emitted with the [`Eva`](#class-eva) client as the first argument. If the [`EvaApplication`](#class-evaapplication) is [`locked`](#lock---this), and there are no more clients connected, the `vacant` event is fired. | ||
#### Options | ||
##### *options.verifyClients* | ||
You may supply a *verifyClients* function which must return `true` for each client wishing to connect to the WebSocket endpoint. The function may have the following two signatures: | ||
```javascript | ||
@@ -188,24 +177,29 @@ function verifyClients(info) { // synchronous | ||
*path* defaults to `"/"` | ||
##### *options.useHeartbeat* | ||
#### .addHandler(function *handler*) -> this | ||
Registers *handler* to be invoked for each new client connection that is made. This is the same as passing a *handler* argument to the [`EvaApplication`](#class-evaapplication) constructor. You may have multiple handlers. | ||
By default, all clients of an [`EvaApplication`](#class-evaapplication) will periodically be sent a heartbeat. If a client endpoint does not respond after 10 heartbeats, the connection will be destroyed. Setting `options.useHeartbeat = false` will cause this instance of [`EvaApplication`](#class-evaapplication) to **not** send and track heartbeats. | ||
#### .removeHandler(function *handler*) -> this | ||
Unregisters a function *handler*. This is the opposite of [`.addHandler()`](#addhandlerfunction-handler---this). | ||
#### .lock() -> this | ||
Prevents new clients from connecting to the websocket endpoint. | ||
#### .currentClients() -> Array | ||
Returns a snapshot array of the current [`eva`](#class-eva) clients that are connected. | ||
#### .unlock() -> this | ||
Allows new clients to connect to the websocket endpoint. When an [`EvaApplication`](#class-evaapplication) instance is created, it starts out unlocked. | ||
#### .kill() -> this | ||
Starts to close down this [`EvaApplication`](#class-evaapplication) by rejecting all new clients trying to connect, and by invoking the [`.kill()`](#kill---this) method on each connected client. | ||
[`Locks`](#lock---this) the [`EvaApplication`](#class-evaapplication), and invokes the [`.kill()`](#kill---this) method on each connected client. | ||
#### .done() -> this | ||
Starts to close down this [`EvaApplication`](#class-evaapplication) by rejecting all new clients trying to connect, and by invoking the [`.done()`](#done---this) method on each connected client. | ||
[`Locks`](#lock---this) the [`EvaApplication`](#class-evaapplication), and invokes the [`.done()`](#done---this) method on each connected client. | ||
#### .broadcast(string *eventName*, [any *data*]) -> this | ||
Invokes the [`.send()`](#sendstring-eventname-any-data---this) method on each client that [`isReady`](#get-isready---boolean). | ||
Invokes the [`.emit()`](#emitstring-eventname-any-data---this) method on each client that [`isReady`](#get-isready---boolean). | ||
#### .currentClients() -> Array | ||
Returns a snapshot array of the current [clients](#class-eva) that are connected. | ||
#### get .isLocked -> boolean | ||
Returns whether the [`EvaApplication`](#class-evaapplication) is currently rejecting new clients. | ||
# License | ||
[MIT](https://github.com/JoshuaWise/eva-events/blob/master/LICENSE) |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
34078
684
196
2
Updatedbluebird@^3.1.2