From 951488adbf5069d884d4f3053c746e136812219c Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Mon, 4 Sep 2017 10:32:52 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Release=20v2.0.0=20(#44)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚀 Overhaul * ⚡️ No need to be passing these values around * 📦 #44 Add `callsite` package in view of adding stack traces to requests/emissions * ✨ #44 Add new utility to get request callsites excepting callable-instance calls * ✨ #44 Add stack trace line to requests and emissions * ⚡️ Parse JSON for emitted event _after_ sending to RabbitMQ in requests * ✨ Add `isCustom` to `parseEvent`, allowing us to mark when we need `started` date setting * ⚡️ Don't map or serialise data in endpoint replies if we don't need to * 💬 Make 'unparsable data' warning for requests and emissions be clear as to what has happened * 🔨 Make Emitter instantiation work the same as Request - just opts/event name * 🐛 Properly set `scheduled` headers when demitting a message via a schedule * 💬 Mini punctuation fix * ⚡️ Parse JSON for emitted events _after_ sending to RabbitMQ * 📦 Adding new dependencies chai, mocha and nyc for testing * 🔧 Update .gitignore to include new .nyc_output directory (new version of istanbul) * ✅ Add new test scripts * ✅ Add test bootstrap * ✅ Adding initial tests for basic types/usage * ✅ Add placeholders for utils testing * ✅ More of a test skeleton for endpoints * 💬 Fix thrown error being scoped to wrong type * 🐛 Safely fall back to an empty options object when parsing endpoint options for the first time * 🐛 Include `utils` folder in npm releases * 🔖 Release v2.0.0-beta.15 * 🐛 Fix incompatibility between Remit 1 and 2 We now check for the message's correlationId rather than relying on the new event style in Remit 2. * 🔖 Release v2.0.0-beta.16 * ✅ Making test throw checks for emissions more specific * ✅ Adding some tests for endpoints * 🚧 Add .eslintrc config file for Codacy etc * ✅ Add basic Remit tests * ✅ Add basic remit.listen tests * ✅ Pad out endpoint middleware testing * 🐛 Correctly merge query string options when parsing AMQP urls * ✅ Add testing to parseAmqpUrl utility * ✅ Add travis testing * ✅ Stick with latest Node 8 - no harmony for nw * ✅ Add coveralls token * 📦 Add new coveralls dev dependency * ✅ Add new `travis` script for testing and pushing coverage reports * 📝 Add badges to README * 🐛 Add proper timeout error object when requests time out * 🔖 Release v2.0.0-beta-17 * :zap: Only internally set one listener for request data/timeout * 💬 Swallow worker channel errors; we should manually log these in the relevant areas * 🐛 Do not consistently redefine differing queue expirations when scheduling emissions * 🔖 Release v2.0.0-beta.18 * ⚡️ No need to wait for an emitter to be ready before sending * 🐛 Fix demission 'schedule' dates not properly being checked for validity * 🔖 Release v2.0.0-beta.19 * 🐛 Fixes #48 - only time to not pass fallback if it's declared is if it's undefined * 🔧 Updating package-lock.json (late) * 🔖 Release v2.0.0-beta.20 * 🔖 Release v2.0.0-beta.21 * 🚿 Cleaning up test linting * 🔖 Bump version to 2.0.0, ready to release --- .coveralls.yml | 1 + .eslintrc | 191 ++ .gitignore | 3 +- .travis.yml | 7 + LICENSE | 21 + README.md | 633 +++-- index.js | 901 +----- lib/Emitter.js | 195 ++ lib/Endpoint.js | 160 ++ lib/Listener.js | 136 + lib/Remit.js | 66 + lib/Request.js | 200 ++ package-lock.json | 2603 ++++++++++++++++++ package.json | 51 +- test/bootstrap.js | 3 + test/emitter.test.js | 209 ++ test/endpoint.test.js | 420 +++ test/exports.test.js | 41 + test/index.js | 162 -- test/listener.test.js | 48 + test/request.test.js | 22 + test/utils/CallableWrapper.test.js | 0 test/utils/asyncWaterfall.test.js | 0 test/utils/genUuid.test.js | 0 test/utils/generateConnectionOptions.test.js | 0 test/utils/getStackLine.test.js | 0 test/utils/handlerWrapper.test.js | 0 test/utils/parseAmqpUrl.test.js | 35 + test/utils/parseEvent.test.js | 0 test/utils/serializeData.test.js | 0 utils/CallableWrapper.js | 38 + utils/ChannelPool.js | 21 + utils/asyncWaterfall.js | 33 + utils/genUuid.js | 7 + utils/generateConnectionOptions.js | 14 + utils/getStackLine.js | 25 + utils/handlerWrapper.js | 39 + utils/parseAmqpUrl.js | 47 + utils/parseEvent.js | 44 + utils/serializeData.js | 5 + 40 files changed, 4994 insertions(+), 1387 deletions(-) create mode 100644 .coveralls.yml create mode 100644 .eslintrc create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 lib/Emitter.js create mode 100644 lib/Endpoint.js create mode 100644 lib/Listener.js create mode 100644 lib/Remit.js create mode 100644 lib/Request.js create mode 100644 package-lock.json create mode 100644 test/bootstrap.js create mode 100644 test/emitter.test.js create mode 100644 test/endpoint.test.js create mode 100644 test/exports.test.js delete mode 100644 test/index.js create mode 100644 test/listener.test.js create mode 100644 test/request.test.js create mode 100644 test/utils/CallableWrapper.test.js create mode 100644 test/utils/asyncWaterfall.test.js create mode 100644 test/utils/genUuid.test.js create mode 100644 test/utils/generateConnectionOptions.test.js create mode 100644 test/utils/getStackLine.test.js create mode 100644 test/utils/handlerWrapper.test.js create mode 100644 test/utils/parseAmqpUrl.test.js create mode 100644 test/utils/parseEvent.test.js create mode 100644 test/utils/serializeData.test.js create mode 100644 utils/CallableWrapper.js create mode 100644 utils/ChannelPool.js create mode 100644 utils/asyncWaterfall.js create mode 100644 utils/genUuid.js create mode 100644 utils/generateConnectionOptions.js create mode 100644 utils/getStackLine.js create mode 100644 utils/handlerWrapper.js create mode 100644 utils/parseAmqpUrl.js create mode 100644 utils/parseEvent.js create mode 100644 utils/serializeData.js diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..0903210 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +repo_token: 0rxp9epmxLHCaWKPvbWyizfsPFdp68Cs5 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..59f3057 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,191 @@ +{ + "parserOptions": { + "ecmaVersion": 8, + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + }, + "sourceType": "module" + }, + + "env": { + "es6": true, + "node": true + }, + + "plugins": [ + "import", + "node", + "promise", + "standard" + ], + + "globals": { + "document": false, + "navigator": false, + "window": false + }, + + "rules": { + "accessor-pairs": "error", + "arrow-spacing": ["error", { "before": true, "after": true }], + "block-spacing": ["error", "always"], + "brace-style": ["error", "1tbs", { "allowSingleLine": true }], + "camelcase": ["error", { "properties": "never" }], + "comma-dangle": ["error", { + "arrays": "never", + "objects": "never", + "imports": "never", + "exports": "never", + "functions": "never" + }], + "comma-spacing": ["error", { "before": false, "after": true }], + "comma-style": ["error", "last"], + "constructor-super": "error", + "curly": ["error", "multi-line"], + "dot-location": ["error", "property"], + "eol-last": "error", + "eqeqeq": ["error", "always", { "null": "ignore" }], + "func-call-spacing": ["error", "never"], + "generator-star-spacing": ["error", { "before": true, "after": true }], + "handle-callback-err": ["error", "^(err|error)$" ], + "indent": ["error", 2, { "SwitchCase": 1 }], + "key-spacing": ["error", { "beforeColon": false, "afterColon": true }], + "keyword-spacing": ["error", { "before": true, "after": true }], + "new-cap": ["error", { "newIsCap": true, "capIsNew": false }], + "new-parens": "error", + "no-array-constructor": "error", + "no-caller": "error", + "no-class-assign": "error", + "no-compare-neg-zero": "error", + "no-cond-assign": "error", + "no-const-assign": "error", + "no-constant-condition": ["error", { "checkLoops": false }], + "no-control-regex": "error", + "no-debugger": "error", + "no-delete-var": "error", + "no-dupe-args": "error", + "no-dupe-class-members": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty-character-class": "error", + "no-empty-pattern": "error", + "no-eval": "error", + "no-ex-assign": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-extra-boolean-cast": "error", + "no-extra-parens": ["error", "functions"], + "no-fallthrough": "error", + "no-floating-decimal": "error", + "no-func-assign": "error", + "no-global-assign": "error", + "no-implied-eval": "error", + "no-inner-declarations": ["error", "functions"], + "no-invalid-regexp": "error", + "no-irregular-whitespace": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": ["error", { "allowLoop": false, "allowSwitch": false }], + "no-lone-blocks": "error", + "no-mixed-operators": ["error", { + "groups": [ + ["==", "!=", "===", "!==", ">", ">=", "<", "<="], + ["&&", "||"], + ["in", "instanceof"] + ], + "allowSamePrecedence": true + }], + "no-mixed-spaces-and-tabs": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }], + "no-negated-in-lhs": "error", + "no-new": "error", + "no-new-func": "error", + "no-new-object": "error", + "no-new-require": "error", + "no-new-symbol": "error", + "no-new-wrappers": "error", + "no-obj-calls": "error", + "no-octal": "error", + "no-octal-escape": "error", + "no-path-concat": "error", + "no-proto": "error", + "no-redeclare": "error", + "no-regex-spaces": "error", + "no-return-assign": ["error", "except-parens"], + "no-return-await": "error", + "no-self-assign": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow-restricted-names": "error", + "no-sparse-arrays": "error", + "no-tabs": "error", + "no-template-curly-in-string": "error", + "no-this-before-super": "error", + "no-throw-literal": "error", + "no-trailing-spaces": "error", + "no-undef": "error", + "no-undef-init": "error", + "no-unexpected-multiline": "error", + "no-unmodified-loop-condition": "error", + "no-unneeded-ternary": ["error", { "defaultAssignment": false }], + "no-unreachable": "error", + "no-unsafe-finally": "error", + "no-unsafe-negation": "error", + "no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true, "allowTaggedTemplates": true }], + "no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": true }], + "no-use-before-define": ["error", { "functions": false, "classes": false, "variables": false }], + "no-useless-call": "error", + "no-useless-computed-key": "error", + "no-useless-constructor": "error", + "no-useless-escape": "error", + "no-useless-rename": "error", + "no-useless-return": "error", + "no-whitespace-before-property": "error", + "no-with": "error", + "object-property-newline": ["error", { "allowMultiplePropertiesPerLine": true }], + "one-var": ["error", { "initialized": "never" }], + "operator-linebreak": ["error", "after", { "overrides": { "?": "before", ":": "before" } }], + "padded-blocks": ["error", { "blocks": "never", "switches": "never", "classes": "never" }], + "prefer-promise-reject-errors": "error", + "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], + "rest-spread-spacing": ["error", "never"], + "semi": ["error", "never"], + "semi-spacing": ["error", { "before": false, "after": true }], + "space-before-blocks": ["error", "always"], + "space-before-function-paren": ["error", "always"], + "space-in-parens": ["error", "never"], + "space-infix-ops": "error", + "space-unary-ops": ["error", { "words": true, "nonwords": false }], + "spaced-comment": ["error", "always", { + "line": { "markers": ["*package", "!", "/", ","] }, + "block": { "balanced": true, "markers": ["*package", "!", ",", ":", "::", "flow-include"], "exceptions": ["*"] } + }], + "symbol-description": "error", + "template-curly-spacing": ["error", "never"], + "template-tag-spacing": ["error", "never"], + "unicode-bom": ["error", "never"], + "use-isnan": "error", + "valid-typeof": ["error", { "requireStringLiterals": true }], + "wrap-iife": ["error", "any", { "functionPrototypeMethods": true }], + "yield-star-spacing": ["error", "both"], + "yoda": ["error", "never"], + + "import/export": "error", + "import/first": "error", + "import/no-duplicates": "error", + "import/no-webpack-loader-syntax": "error", + + "node/no-deprecated-api": "error", + "node/process-exit-as-throw": "error", + + "promise/param-names": "error", + + "standard/array-bracket-even-spacing": ["error", "either"], + "standard/computed-property-even-spacing": ["error", "even"], + "standard/no-callback-literal": "error", + "standard/object-curly-even-spacing": ["error", "either"] + } +} diff --git a/.gitignore b/.gitignore index b38069d..b419673 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,9 @@ pids # Directory for instrumented libs generated by jscoverage/JSCover lib-cov -# Coverage directory used by tools like istanbul +# Coverage directories used by tools like istanbul coverage +.nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4c8501c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +services: rabbitmq +sudo: required +node_js: + - "node" +cache: yarn +script: "npm run travis" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..eddf64d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Jack Williams + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index b738de8..156ea03 100644 --- a/README.md +++ b/README.md @@ -1,425 +1,446 @@ -![Dependencies](https://david-dm.org/jpwilliams/remit.svg) -![Downloads](https://img.shields.io/npm/dm/remit.svg) +# remit -# What's Remit? -A small set of functionality used to create microservices that don't need to be aware of one-another's existence. It uses AMQP at its core to manage service discovery-like behaviour without the need to explicitly connect one service to another. +[![Build Status](https://travis-ci.org/jpwilliams/remit.svg?branch=v2)](https://travis-ci.org/jpwilliams/remit) [![Coverage Status](https://coveralls.io/repos/github/jpwilliams/remit/badge.svg?branch=v2)](https://coveralls.io/github/jpwilliams/remit?branch=v2) [![npm downloads per month](https://img.shields.io/npm/dm/remit.svg)](https://www.npmjs.com/package/remit) [![npm version](https://img.shields.io/npm/v/remit.svg)](https://www.npmjs.com/package/remit) -# Contents +A wrapper for RabbitMQ for communication between microservices. No service discovery needed. -* [Simple usage](#simple-usage) -* [Pre-requisites](#pre-requisites) -* [Installation](#installation) -* [Key examples](#key-examples) -* [API reference](#api-reference) -* [Improvements](#improvements) +``` sh +npm install remit +``` -# Simple usage +``` js +remit + .endpoint('user') + .handler((event) => { + return { + name: 'Jack Williams', + email: 'jack@wildfire.gg' + } + }) + +// another service/process +const user = await remit.request('user')() +console.log(user) + +/* { + name: 'Jack Williams', + email: 'jack@wildfire.gg' +} */ +``` -`remit` makes use of four simple commands: `req` (request), `res` (respond), `emit` and `listen`. +--- -* `req` requests data from a defined endpoint which, in turn, is created using `res` -* `listen` waits for messages `emit`ted from anywhere in the system. +## What's remit? -A connection to your AMQP server's required before you can get going, but you can easily do that! +A simple wrapper over [RabbitMQ](http://www.rabbitmq.com) to provide [RPC](https://en.wikipedia.org/wiki/Remote_procedure_call) and [ESB](https://en.wikipedia.org/wiki/Enterprise_service_bus)-style behaviour. -```javascript -const remit = require('remit')({ - name: 'my_service', // this is required for a service that has a listener - url: 'amqp://localhost' -}) -``` +It supports **request/response** calls (e.g. requesting a user's profile), **emitting events** to the entire system (e.g. telling any services interested that a user has been created) and basic **scheduling** of messages (e.g. recalculating something every 5 minutes), all **load balanced** sacross grouped services and redundant; if a service dies, another will pick up the slack. -After that, the world is yours! Here are some basic examples of the four commands mentioned above. - -```javascript -// API -remit.req('add', { - first: 2, - second: 7 -}, function (err, data) { - console.log('The result is ' + data) -}) - -// Server -remit.res('add', function (args, done) { - done(null, (args.first + args.second)) - - remit.emit('something.happened', args) -}) - -// Listener 1 -remit.listen('something.happened', function (args, done) { - console.log(args) - - // We return done() to acknowledge that the task has been completed - return done() -}) - -// Listener 2 -remit.listen('something.#', function (args) { - console.log('Something... did something...') - - return done() -}) - -/* - 1. The API requests the 'add' endpoint. - 2. The Server responds with the result of the sum. - 3. The API logs 'The result is 9'. - 4. The Server emits the 'something.happened' event. - 5. Listener 1 logs the arguments the API sent. - 6. Listener 2 logs 'Something... did something...'. -*/ -``` +There are four types you can use with Remit. -# Pre-requisites +* [request](#), which fetches data from an [endpoint](#) +* [emit](#), which emits data to [listen](#)ers -To use `remit` you'll need: -* A _RabbitMQ_ server (_Remit_ `1.2.0+` requires `>=3.4.0`) -* _Node v4.x.x_ -* _npm_ +Endpoints and listeners are grouped by "Service Name" specified as `name` or the environment variable `REMIT_NAME` when creating a Remit instance. This grouping means only a single consumer in that group will receive a message. This is used for scaling services. -# Installation +--- -Once your _RabbitMQ_ server's up and running, simply use `npm` to install `remit`! -```javascript -npm install remit -``` +## Contents + +* [What's remit?](#) +* [Recommendations](#) +* [API/Usage](#) +* [Events](#) +* [Handlers](#) -# Key examples +--- -There are two methods for sending messages with `remit`: _request_ or _emit_. +## API/Usage -A _request_ implies that the requester wants a response back, whereas using an _emission_ means you wish to notify other services of an event without requiring their input. +* [request(event)](#) + * [request.on(eventName, listener)](#) + * [request.fallback(data)](#) + * [request.options(options)](#) + * [request.ready()](#) + * [request.send([data[, options]]) OR request([data[, options]])](#) +* [endpoint(event[, ...handlers])](#) + * [endpoint.handler(...handlers)](#) + * [endpoint.on(eventName, listener)](#) + * [endpoint.options(options)](#) + * [endpoint.start()](#) +* [emit(event)](#) + * [emit.on(eventName, listener)](#) + * [emit.options(options)](#) + * [emit.ready()](#) + * [emit.send([data[, options]]) OR emit([data[, options]])](#) +* [listen(event[, ...handlers])](#) + * [listen.handler(...handlers)](#) + * [listen.on(eventName, listener)](#) + * [listen.options(options)](#) + * [listen.start()](#) -Let's start with a simple authentication example. We'll set up an API that our user can request to log in. +--- -```javascript -// Import remit and connect to our AMQP server -const remit = require('remit')() +#### `request(event)` -// Import whatever HTTP API creator we want -const api = require('some-api-maker') +* `event` <string> | <Object> -// Set up a route using our API creator -api.get('/login', function (req, res) { - // Send a request via remit to the 'user.login' endpoint - remit.req('user.login', { - username: req.username, - password: req.password - }, function (err, data) { - //If there's something wrong... - if (err) return res.failure(err) +Create a new request for data from an [endpoint](#) by calling the event dictated by `event`. If an object is passed, `event` is required. See [`request.options`](#) for available options. - // Otherwise, woohoo! We're logged in! - return res.success(data.user) - }) -}) +``` js +remit.request('foo.bar') ``` -Awesome! Now we'll set up the authentication service that'll respond to the request. +`timeout` and `priority` are explained and can be changed at any stage using [`request.options()`](#). -```javascript -// Import remit and connect to our AMQP server -const remit = require('remit')() +The request is sent by running the returned function (synonymous with calling `.send()`), passing the data you wish the make the request with. -// Respond to 'user.login' events -remit.res('user.login', function (args, done) { - // If it's not Mr. Bean, send back an error! - if (args.username !== 'Mr. Bean') return done('You\'re not Mr. Bean!') +For example, to retrieve a user from the `'user.profile'` endpoint using an ID: - // Otherwise, let's "log in" - done(null, { - username: 'Mr. Bean', - birthday: '14/06/1961' - }) -}) +``` js +const getUserProfile = remit.request('user.profile') +const user = await getUserProfile(123) +console.log(user) +// prints the user's data ``` -Done. That's it. Our `API` service will request an answer to the `user.login` endpoint and our server will respond. Simples. +Returns a new request. + +#### `request.on(eventName, listener)` -Let's now say that we want a service to listen out for if it's a user's birthday and send them an email if they've logged in on that day! With most other systems, this would require adding business logic to our login service to explicitly call some `birthday` service and check, but not with `remit`. +* `eventName` <any> +* `listener` <Function> -At the end of our `authentication` service, let's add an emission of `user.login.success`. +Subscribe to this request's dumb EventEmitter. For more information on the events emitted, see the [Events](#) section. -```javascript -// Respond to 'user.login' events -remit.res('user.login', function (args, done) { - // If it's not Mr. Bean, send back an error! - if (args.username !== 'Mr. Bean') return done('You\'re not Mr. Bean!') +Returns a reference to the `request`, so that calls can be chained. - // Otherwise, let's "log in" - let user = { - username: 'Mr. Bean', - birthday: '14/06/1961' - } +#### `request.fallback(data)` - done(null, user) +* `data` <any> - // After we've logged the user in, let's emit that everything went well! - remit.emit('user.login.success', { user }) -}) +Specifies data to be returned if a request fails for any reason. Can be used to gracefully handle failing calls across multiple requests. When a fallback is set, any request that fails will instead resolve successfully with the data passed to this function. + +``` js +const request = remit + .request('user.list') + .fallback([]) ``` -Now that we've done that, _any_ other services on the network can listen in on that event and react accordingly! +The error is still sent over the request's EventEmitter, so listening to `'error'` lets you handle the error however you wish. -Let's make our `birthday` service. +You can change the fallback at any point in a request's life and unset it by explicitly passing `undefined`. -```javascript -const remit = require('remit')({ - name: 'birthday' -}) +Returns a reference to the `request`, so that calls can be chained. -const beanmail = require('send-mail-to-mr-bean') +#### `request.options(options)` -remit.listen('user.login.success', function (args, done) { - let today = '14/06/1961' +* `options` <Object> + * `event` <string> **Required** + * `timeout` <integer> **Default:** `30000` + * `priority` <integer> **Default:** `0` - if (today === args.user.birthday) { - beanmail.send() - } +Set various options for the request. Can be done at any point in a request's life but will not affect timeouts in which requests have already been sent. - return done() -}) +``` js +const request = remit + .request('foo.bar') + .options({ + timeout: 5000 + }) ``` -Sorted. Now every time someone logs in successfully, we run a check to see if it's their birthday. - -Emissions can be hooked into by any number of different services, but only one "worker" per service will receive each emission. +Settings `timeout` to `0` will result in there being no timeout. Otherwise it is the amount of time in milliseconds to wait before declaring the request "timed out". -So let's also start logging every time a user performs _any_ action. We can do this by using the `#` wildcard. +`priority` can be an integer between `0` and `10`. Higher priority requests will go to the front of queues over lower priority requests. -```javascript -const remit = require('remit')({ - name: 'logger' -}) +Returns a reference to the `request`, so that calls can be chained. -let user_action_counter = 0 +#### `request.ready()` -remit.listen('user.#', function (args, done) { - user_action_counter++ +Returns a promise which resolves when the request is ready to make calls. - return done() -}) +``` js +const request = await remit + .request('foo'.bar') + .ready() ``` -# API reference +Any calls made before this promise is resolved will be automatically queued until it is. + +Returns a reference to the `request`, so that calls can be chained. -* [`Remit`](#requireremitoptions) - Instantiate Remit -* [`req`](#reqendpoint-data-callback-options--timeout-5000) - Make a request to an endpoint -* [`treq`](#treqendpoint-data-callback-options--timeout-5000) - Make a transient request to an endpoint -* [`res`](#resendpoint-callback-context-options--queuename-my_queue) - Define an endpoint -* [`emit`](#emitevent-data-options) - Emit to all listeners of an event -* [`demit`](#demitevent-eta-data-options) - Emit to all listeners of an event at a specified time -* [`listen`](#listenevent-callback-context-options--queuename-my_queue) - Listen to emissions of an event +#### `request.send([data[, options]])` -## require('remit')([options]) +_Synonymous with `request([data[, options]])`_ -Creates a Remit object, with the specified `options` (if any), ready for use with further functions. +* `data` <any> **Default:** `null` +* `options` <Object> -#### Arguments +Sends a request. `data` can be anything that plays nicely with [JSON.stringify](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify). If `data` is not defined, `null` is sent (`undefined` cannot be parsed into JSON). -* `options` - _Optional_ An object containing options to give to the Remit instantiation. Currently-acceptable options are: - * `name` - The name to give the current service. This is used heavily for load balancing requests, so instances of the same service (that should load balance requests between themselves) should have the same name. Is _required_ if using [`listen`](#listenevent-callback-context-options--queuename-my_queue). - * `url` - The URL to use to connect to the AMQ. Defaults to `amqp://localhost`. - * `connection` - If you already have a valid AMQ connection, you can provide and use it here. The use cases for this are slim but present. - * `prefetch` - The number of messages a service should hold in memory before waiting for an acknowledgement. Defaults to `128`. +``` js +const getUser = remit.request('user.getProfile') -## req(endpoint, data, [callback], [options = {timeout: 5000}]) +// either of these perform the same action +const user = await getUser(123) +const user = await getUser.send(123) +``` -Makes a request to the specified `endpoint` with `data`, optionally returning a `callback` detailing the response. It's also possible to provide `options`, namely a `timeout`. +`options` can contain anything provided in [`request.options`](#), but the options provided will only apply to that single request. -#### Arguments +Returns a promise that resolves with data if the request was successful or rejects with an error if not. Always resolves if a [fallback](#) is set. -* `endpoint` - A string endpoint that can be defined using [`res`](#resendpoint-callback-context-options--queuename-my_queue). -* `data` - Can be any of `boolean`, `string`, `array` or `object` and will be passed to the responder. -* `callback(err, data)` - _Optional_ A callback which is called either when the responder has handled the message or the message "timed out" waiting for a response. In the case of a timeout, `err` will be populated, though the responder can also explicitly control what is sent back in both `err` and `data`. -* `options` - _Optional_ Supply an object here to explicitly define certain options for the AMQ message. `timeout` is the amount of time in milliseconds to wait for a response before returning an error. There is currently only one _defined_ use case for this, though it gives you total freedom as to what options you provide. +--- -#### Examples +#### `endpoint(event[, ...handlers])` -```javascript -// Calls the 'user.profile', endpoint, but doesn't ask for a response. -remit.req('user.profile', { - username: 'jacob123' -}) -``` +* `event` <string> | <Object> +* `...handlers` <Function> + +Creates an endpoint that replies to [`request`](#)s. -```javascript -// Calls the 'user.profile' endpoint asking for a response but timing out after the default of 5 seconds. -remit.req('user.profile', { - username: 'jacob123' -}, (err, data) => { - if (err) console.error('Oh no! Something went wrong!', err) +`event` is the code requests will use to call the endpoint. If an object is passed, `event` is required. For available options, see [`endpoint.options`](#). - return console.log('Got the result back!', data) -}) +``` js +const endpoint = await remit + .endpoint('foo.bar', console.log) + .start() ``` -```javascript -// Calls the 'user.profile', endpoint asking for a response but timing out after a custom wait of 20 seconds. -remit.req('user.profile', { - username: 'jacob123' -}, (err, data) => { - if (err) console.error('Oh no! Something went wrong!', err) - - return console.log('Got the result back!', data) -}, { - timeout: 20000 -}) +[`start()`](#) must be called on an endpoint to "boot it up" ready to receive requests. An endpoint that's started without a `handler` (a function or series of functions that returns data to send back to a request) will throw. You can set handlers here or using [`endpoint.handler`](#). To learn more about handlers, check the [Handlers](#) section. + +Returns a new endpoint. + +#### `endpoint.handler(...handlers)` + +* `...handlers` <Function> + +Set the handler(s) for this endpoint. Only one series of handlers can be active at a time, though the active handlers can be changed using this call at any time. + +``` js +const endpoint = remit.endpoint('foo.bar') +endpoint.handler(logRequest, sendFoo) +endpoint.start() ``` -#### AMQ behaviour +For more information on handlers, see the [Handlers](#) section. -1. Confirms connection and exchange exists. -2. If a callback's provided, confirm the existence of and consume from a "result queue" specific to this process. -3. Publish the message using the provided `endpoint` as a routing key. +Returns a reference to the `endpoint`, so that calls can be chained. -## treq(endpoint, data, [callback], [options = {timeout: 5000}]) +#### `endpoint.on(eventName, listener)` -Identical to [`req`](#reqendpoint-data-callback-options--timeout-5000) but will remove the request message upon timing out. Useful for calls from APIs. For example, if a client makes a request to delete a piece of content but that request times out, it'd be jarring to have that action suddenly undertaken at an unspecified interval afterwards. `treq` is useful for avoiding that circumstance. +* `eventName` <any> +* `listener` <Function> -#### AMQ behaviour +Subscribe to this endpoint's dumb EventEmitter. For more information on the events emitted, see the [Events](#) section. -Like [`req`](#reqendpoint-data-callback-options--timeout-5000) but adds an `expiration` field to the message. +Returns a reference to the `endpoint`, so that calls can be chained. -## res(endpoint, callback, [context], [options = {queueName: 'my_queue'}]) +#### `endpoint.options(options)` -Defines an endpoint that responds to [`req`](#reqendpoint-data-callback-options--timeout-5000)s. Returning the provided `callback` is a nessecity regardless of whether the requester wants a response as it is to used to acknowledge messages as being handled. +#### `endpoint.start()` -#### Arguments +---- -* `endpoint` - A string endpoint that requetsers will use to reach this function. -* `callback(args, done)` - A callback containing data from the requester in `args` and requiring the running of `done(err, data)` to signify completion regardless of the requester's requirement for a response. -* `context` - _Optional_ The context in which `callback(args, done)` will be called. -* `options` - _Optional_ An object that can contain a custom queue to listen for messages on. +#### `emit.on(eventName, listener)` -#### Examples +* `eventName` <any> +* `listener` <Function> -```javascript -// Defines the 'user.profile' profile endpoint, retrieving a user from our dummy database -remit.res('user.profile', function (args, done) { - if (args.username) return done('No username provided!') +Subscribe to this emitter's dumb EventEmitter. For more information on the events emitted, see the [Events](#) section. - mydb.get_user(args.username, function (err, user) { - return done(err, user) - }) -}) -``` +Returns a reference to the `emit`, so that calls can be chained. + +#### `emit.options(options)` + +#### `emit.ready()` + +#### `emit.send([data[, options]])` -#### AMQ behaviour +--- -1. Confirms connection and exchange exists. -2. Binds to and consumes from the queue with the name defined by `endpoint` +#### `listen.handler(...handlers)` -## emit(event, [data], [options]) +#### `listen.on(eventName, listener)` -Emits to all [`listen`](#listenevent-callback-context-options--queuename-my_queue)ers of the specified event, optionally with some `data`. This is essentially the same as [`req`](#reqendpoint-data-callback-options--timeout-5000) but no `callback` can be defined and `broadcast` is set to `true` in the message options. +* `eventName` <any> +* `listener` <Function> -#### Arguments +Subscribe to this listener's dumb EventEmitter. For more information on the events emitted, see the [Events](#) section. -* `event` - The "event" to emit to [`listen`](#listenevent-callback-context-options--queuename-my_queue)ers. -* `data` - _Optional_ Data to send to [`listen`](#listenevent-callback-context-options--queuename-my_queue)ers. Can be any of `boolean`, `string`, `array` or `object`. -* `options` - _Optional_ Like [`req`](#reqendpoint-data-callback-options--timeout-5000), supply an object here to explicitly define certain options for the AMQ message. +Returns a reference to the `listen`, so that calls can be chained. -#### Examples +#### `listen.options(options)` -```javascript -// Emits the 'user.registered' event to all listeners -remit.emit('user.registered') +#### `listen.start()` + +## Events + +[`request`](#), [`endpoint`](#), [`emit`](#) and [`listen`](#) all export EventEmitters that emit events about their incoming/outgoing messages. + +All of the events can be listened to by using the `.on()` function, providing an `eventName` and a `listener` function, like so: + +``` js +const request = remit.request('foo.bar') +const endpoint = remit.endpoint('foo.bar') +const emit = remit.emit('foo.bar') +const listen = remit.listen('foo.bar') + +request.on('...', ...) +endpoint.on('...', ...) +emit.on('...', ...) +listen.on('...', ...) ``` -```javascript -// Emits the 'user.registered' event, supplying some of the user's basic information -remit.emit('user.registered', { - username: 'jacob123', - name: 'Jacob Four', - email: 'jacob@five.com', - website: 'www.six.com' -}) +Events can also be listened to _globally_, by adding a listener directly to the type. This listener will receive events for all instances of that type. This makes it easier to introduce centralised logging to remit's services. + +``` js +remit.request.on('...', ...) +remit.endpoint.on('...', ...) +remit.emit.on('...', ...) +remit.listen.on('...', ...) ``` -#### AMQ behaviour +The following events can be listened to: -1. Confirms connection and exchange exists. -2. Publish the message using the provided `endpoint` as a routing key and with the `broadcast` option set to `true`. +| Event | Description | Returns | request | endpoint | emit | listen | +| ----- | ----------- | ------- | :---: | :---: | :---: | :---: | +| `data` | Data was received | Raw data | ✅ | ✅ | ❌ | ✅ | +| `error` | An error occured or was passed back from an endpoint | Error | ✅ | ✅ | ✅ | ✅ | +| `sent` | Data was sent | The event that was sent | ✅ | ✅ | ✅ | ❌ | +| `success` | The action was successful | The successful result/data | ✅ | ✅ | ✅ | ✅ | +| `timeout` | The request timed out | A [timeout object](#) | ✅ | ❌ | ❌ | ❌ | -## demit(event, eta, [data], [options]) +## Handlers -Like [`emit`](#emitevent-data-options) but tells [`listen`](#listenevent-callback-context-options--queuename-my_queue)ers to wait until `eta` to running their respective functions. Similar in design and functionality to [Celery's `eta` usage](http://docs.celeryproject.org/en/latest/userguide/calling.html#eta-and-countdown). Largely useful for tasks that should repeat like session health checks. +[Endpoints](#) and [listeners](#) use handlers to reply to or, uh, handle incoming messages. In both cases, these are functions that can be passed when creating the listener or added/changed real-time by using the `.handler()` method. -#### Arguments +All handlers are passed two items: `event` and `callback`. If `callback` is mapped, you will need to call it to indicate success/failure (see [Handling completion](#) below). If you do not map a callback, you can reply synchronously or by returning a Promise. -* `event` - The "event" to emit to [`listen`](#listenevent-callback-context-options--queuename-my_queue)ers. -* `eta` - A `date` object being the earliest time you wish listeners to respond to the emission. -* `data` - _Optional_ Data to send to [`listen`](#listenevent-callback-context-options--queuename-my_queue)ers. Can be any of `boolean`, `string`, `array` or `object`. -* `options` - _Optional_ Like [`req`](#reqendpoint-data-callback-options--timeout-5000), supply an object here to explicitly define certain options for the AMQ message. +Handlers are used for determining when a message has been successfully dealt with. Internally, Remit uses this to ascertain when to draw more messages in from the queue and, in the case of listeners, when to remove the message from the server. -#### Examples +RabbitMQ gives an at-least-once delivery guarantee, meaning that, ideally, listeners are idempotent. If a service dies before it has successfully returned from a handler, all messages it was processing will be passed back to the server and distributed to another service (or the same service once it reboots). -```javascript -// Emits a "health.check" event that should be processed in 24 hours -let tomorrow = new Date() -tomorrow.setDate(tomorrow.getDate() + 1) +#### Simple returns -remit.demit('health.check', tomorrow) +Here, we create a simple endpoint that returns `{"foo": "bar"}` whenever called: + +``` js +const endpoint = await remit + .endpoint('foo.bar', () => { + return {foo: 'bar'} + }) + .start() ``` -```javascript -// Emits a "health.check" event that should be processed in 24 hours, providing some relevant data -let tomorrow = new Date() -tomorrow.setDate(tomorrow.getDate() + 1) +#### Incoming data + +We can also parse incoming data and gather information on the request by using the given `event` object. -remit.demit('health.check', tomorrow, { - current_health: 52 -}) +``` js +const endpoint = await remit + .endpoint('foo.bar', (event) => { + console.log(event) + }) + .start() ``` -#### AMQ behaviour +#### Event object + +When called, the above will log out the event it's been passed. Here's an example of an event object: -Like [`emit`](#emitevent-data-options) but adds a `timestamp` field to the message which is understood by [`listen`](#listenevent-callback-context-options--queuename-my_queue)-based functions. +``` js +{ + // the time the message was taken from the server + started: , -## listen(event, callback, [context], [options = {queueName: 'my_queue'}]) + // a unique ID for the message + // (useful for idempotency purposes) + eventId: '01BQ5MRBJJ2AK9N23BW4S84WN1', -Listens to events emitted using [`emit`](#emitevent-data-options). Listeners are grouped for load balancing using their `name` provided when instantiating Remit. + // the eventName used to call this endpoint/listener + // (useful when using wildcard listeners) + eventType: 'foo.bar', -While listeners can't sent data back to the [`emit`](#emitevent-data-options)ter, calling the `callback` is still required for confirming successful message delivery. + // the name of the service that called/emitted this + resource: 'service-user', -#### Arguments + // the data sent with the request + data: {userId: 123}, -* `event` - The "event" to listen for emissions of. -* `callback(args, done)` - A callback containing data from the emitter in `args` and requiring the running of `done(err)` to signify completion. -* `context` - _Optional_ The context in which `callback(args, done)` will be called. -* `options` - _Optional_ An object that can contain a custom queue to listen for messages on. + // the time the message was created + timestamp: +} +``` + +#### Handling completion -#### Examples +Handlers provide you with three different ways of showing completion: Promises, callbacks or a synchronous call. To decide what the handler should treat as a successful result, remit follows the following pattern: -```javascript -// Listens for the "user.registered" event, logging the outputted data -remit.listen('user.registered', function (args, done) { - console.log('User registered!', args) - - return done() -}) +``` +if handler does not map second (callback) property: +├── if handler returns a promise: +│ └── Watch resolution/rejection of result +│ else: +│ └── Return synchronous result +else: +└── Wait for callback to be called ``` -#### AMQ behaviour +In any case, if an exception is thrown or an error is passed as the first value to the callback, then the error is passed back to the requester (if an endpoint) or the message sent to a dead-letter queue (if a listener). -1. Confirms connection and exchange exists. -2. Sets a service-unique queue name and confirms it exists -3. Binds the queue to the routing key defined by `event` and starts consuming from said queue +#### Middleware -# Improvements +You can provide multiple handlers in a sequence to act as middleware, similar to that of Express's. Every handler in the line is passed the same `event` object, so to pass data between the handlers, mutate that. -`remit`'s in its very early stages. Basic use is working well, but here are some features I'm looking at implementing to make things a bit more diverse. +A common use case for middleware is validation. Here, a middleware handler adds a property to incoming data before continuing: -* Ability to specify exchange per connection, endpoint or event -* Cleaner error handling (along with some standards) -* ~~Removal of all use of `process.exit()`~~ -* Connection retrying when losing connection to the AMQ -* ~~Use promises instead of callbacks~~ -* Warnings for duplicate `req` subscriptions -* ~~Better handling of `req` timeouts~~ -* Ability for emissions to receive (multiple) results from listeners if required (I really want to use generators for this) -* Obey the `JSON-RPC 2.0` spec -* Tests! +``` js +const endpoint = await remit + .endpoint('foo.bar') + .handler((event) => { + event.foo = 'bar' + }, (event) => { + console.log(event) + // event will contain `foo: 'bar'` + + return true + }) + .start() +``` + +When using middleware, it's important to know how to break the chain if you need to. If anything other than `undefined` is returned in any handler (middleware or otherwise via a Promise/callback/sync call), the chain will break and that data will be returned to the requester. + +If an exception is thrown at any point, the chain will also break and the error will be returned to the requester. + +This means you can fall out of chains early. Let's say we want to fake an empty response for user #21: + +``` js +const endpoint = await remit + .endpoint('foo.bar') + .handler((event) => { + if (event.data.userId === 21) { + return [] + } + }, (event) => { + return calculateUserList() + }) + .start() +``` + +Or perhaps exit if a call is done with no authorisation token: + +``` js +const endpoint = await remit + .endpoint('foo.bar') + .handler(async (event) => { + if (!event.data.authToken) { + throw new Error('No authorisation token given') + } + + event.data.decodedToken = await decodeAuthToken(event.data.authToken) + }, (event) => { + return performGuardedCall() + }) +``` diff --git a/index.js b/index.js index 0b0bbec..fe352e6 100644 --- a/index.js +++ b/index.js @@ -1,900 +1,7 @@ -'use strict' +const Remit = require('./lib/Remit') -const os = require('os') -const uuid = require('uuid').v4 -const trace = require('stack-trace') -const amqplib = require('amqplib/callback_api') -const Pool = require('pool2') - -module.exports = function (opts) { - return new Remit(opts) -} - - - - - - -function Remit (opts) { - if (!opts) opts = {} - const self = this - - // Exposed items - this._service_name = opts.name || '' - this._url = opts.url || 'amqp://localhost' - this._trace = opts.trace === false ? false : true - this._exchange_name = opts.exchange || 'remit' - this._prefetch = parseInt(opts.prefetch) - if (isNaN(this._prefetch)) this._prefetch = 128 - - // Global items - this._connection = opts.connection || null - this._consume_channel = null - this._publish_channel = null - this._exchange = null - - // Callback queues - this._connection_callbacks = [] - this._exchange_callbacks = [] - this._consume_channel_callbacks = [] - this._publish_channel_callbacks = [] - this._work_channel_callbacks = [] - - // Callback trackers - this._results_callbacks = {} - this._results_timeouts = {} - - // States - this._consuming_results = false - this._listener_counts = {} - - // Temp channels - this._worker_pool = new Pool({ - acquire: (callback) => { - self.__connect(() => { - self._connection.createChannel((err, channel) => { - if (err) return callback(err) - - channel.on('error', () => {}) - channel.on('close', () => { - channel.closed = true - }) - - return callback(null, channel) - }) - }) - }, - - dispose: (channel, callback) => { - process.nextTick(() => { - if (channel.closed) return callback() - channel.close(callback) - }) - }, - - min: 5, - max: 10 - }) - - return this -} - - - - - - -Remit.prototype.on_error = null -Remit.prototype.version = require('./package.json').version - - - - - - -Remit.prototype.res = function res (event, callbacks, context, options) { - const self = this - - // Set up default options if we haven't been given any. - options = options || {} - - self.__connect(() => { - self.__assert_exchange(() => { - const chosen_queue = options.queueName || event - - const queueOptions = { - durable: (options.hasOwnProperty('durable')) ? !!options.durable : true, - autoDelete: (options.hasOwnProperty('autoDelete')) ? !!options.autoDelete : false, - exclusive: (options.hasOwnProperty('exclusive')) ? !!options.exclusive : false - } - - const consumerOptions = { - noAck: (options.hasOwnProperty('noAck')) ? !!options.noAck : false, - exclusive: (options.hasOwnProperty('exclusive')) ? !!options.exclusive : false - } - - self.__use_consume_channel(() => { - // TODO Check this for a valid response - self._consume_channel.assertQueue(chosen_queue, queueOptions) - }) - - self.__use_consume_channel(() => { - self._consume_channel.bindQueue(chosen_queue, self._exchange_name, event, {}, (err, ok) => { - if (err) { - console.error(err) - } - - self._consume_channel.consume(chosen_queue, (message) => { - if (message === null) { - throw new Error('Consumer cancelled') - } - - if (!message.properties.timestamp) { - self.__consume_res(message, callbacks, context, consumerOptions) - } else { - const time_to_wait = parseInt(message.properties.timestamp - new Date().getTime()) - - if (time_to_wait <= 0) { - self.__consume_res(message, callbacks, context, consumerOptions) - } else { - setTimeout(() => { - self.__consume_res(message, callbacks, context, consumerOptions) - }, time_to_wait) - } - } - }, consumerOptions) - }) - }) - }) - }) -} - - - - - - -Remit.prototype.req = function req (event, args, callback, options, caller) { - const self = this - - if (!options) { - options = {} - } - - if (self._trace) { - if (!caller) { - caller = trace.get(Remit.prototype.req)[0].toString() - } - - options.appId = self._service_name - options.messageId = caller - options.type = event - } - - options.headers = options.headers || {} - options.headers.uuid = uuid() - - self.__connect(() => { - self.__assert_exchange(() => { - if (!callback) { - return self.__use_publish_channel(() => { - self._publish_channel.publish(self._exchange_name, event, new Buffer(JSON.stringify(args || {})), options) - }) - } - - if (!self._consuming_results) { - self._consuming_results = true - - self.__use_publish_channel(() => { - self._publish_channel.consume('amq.rabbitmq.reply-to', function (message) { - self.__on_result.apply(self, arguments) - }, { - exclusive: true, - noAck: true - }, (err, ok) => { - if (err) { - console.warn(err) - } else { - send_message() - } - }) - }) - } else { - send_message() - } - - function send_message () { - const correlation_id = uuid() - - self._results_callbacks[correlation_id] = { - callback: callback, - context: null, - autoDeleteCallback: true - } - - options.mandatory = true - options.replyTo = 'amq.rabbitmq.reply-to' - options.correlationId = correlation_id - - self._results_timeouts[correlation_id] = setTimeout(function () { - if (!self._results_callbacks[correlation_id]) { - return - } - - delete self._results_callbacks[correlation_id] - delete self._results_timeouts[correlation_id] - - try { - callback({ - event: event, - args: args, - options: options, - message: `Timed out after no response for ${options.timeout || 5000}ms` - }) - } catch (e) { - if (self.on_error) { - self.on_error(e) - } else { - throw e - } - } - }, options.timeout || 5000) - - self.__use_publish_channel(() => { - self._publish_channel.publish(self._exchange_name, event, new Buffer(JSON.stringify(args || {})), options) - }) - } - }) - }) -} - - - - - - -Remit.prototype.listen = function listen (event, callback, context, options) { - const self = this - - if (!self._service_name) { - throw new Error('Must provide a service name if listening') - } - - if (!options) { - options = {} - } - - self._listener_counts[event] = self._listener_counts[event] || 0 - options.queueName = `${event}:emission:${self._service_name}:${++self._listener_counts[event]}` - - self.res.call(self, event, callback, context, options) -} - - - - - - -Remit.prototype.ares = function ares (event, callback, context, options) { - const self = this - options = options || {} - options.noAck = true - - self.res.call(self, event, callback, context, options) -} - - - - - - -Remit.prototype.alisten = function ares (event, callback, context, options) { - const self = this - options = options || {} - options.noAck = true - - self.listen.call(self, event, callback, context, options) -} - - - - - - -Remit.prototype.emit = function emit (event, args, options) { - const self = this - - if (!options) { - options = {} - } - - options.broadcast = true - options.autoDeleteCallback = options.ttl ? false : true - - let caller - - if (self._trace) { - caller = trace.get(Remit.prototype.emit)[0].toString() - } - - self.req.call(self, event, args, options.onResponse, options, caller) -} - - - - - - -Remit.prototype.demit = function demit (event, delay, args, options) { - const self = this - - if (!options) { - options = {} - } - - options.broadcast = true - options.autoDeleteCallback = options.ttl ? false : true - - if (Object.prototype.toString.call(delay) === '[object Date]') { - options.timestamp = delay.getTime() - } - - let caller - - if (self._trace) { - caller = trace.get(Remit.prototype.demit)[0].toString() - } - - self.req.call(self, event, args, options.onResponse, options, caller) -} - - - - - - -Remit.prototype.treq = function treq (event, args, callback, options) { - const self = this - - if (!options) { - options = {} - } - - if (!options.expiration) { - options.expiration = 5000 - } - - if (!options.timeout) { - options.timeout = 5000 - } - - let caller - - if (self._trace) { - caller = trace.get(Remit.prototype.treq)[0].toString() - } - - self.req(event, args, callback, options, caller) -} - - - - - - -Remit.prototype.__connect = function __connect (callback) { - const self = this - - // If no callback was given, we still pretend there - // is one. We use this to signify queue presence. - if (!callback) { - callback = function () {} - } - - // If a connection already exists - if (self._connection) { - // If there are still callbacks being processed, - // hop into the queue; no need to trigger it now. - // Be British and get in line! - if (self._connection_callbacks.length) { - self._connection_callbacks.push(callback) - - return - } - - // Otherwise we do need to trigger now. We missed - // the queue. #awkward - return callback() - } - - // If we're here, a connection doesn't currently exist. - // Now we check whether we're the first call to do this. - // If we are, we'll be the ones to try and connect. - const first = !self._connection_callbacks.length - - // Push our callback in to the queue, eh? - self._connection_callbacks.push(callback) - - if (!first) { - return - } - - let connection_options = {} - - if (self._service_name) { - connection_options.clientProperties = { - connection_name: self._service_name - } - } - - // So let's connect! - amqplib.connect(self._url, connection_options, (err, connection) => { - if (err) { - throw err - } - - // Everything's go fine, so we'll set this global - // object to our new connection. - self._connection = connection - - // Time to run the callbacks. Let's run them and - // take them out of the queue. - // Loop through and make everything happen! - while (self._connection_callbacks.length > 0) { - self._connection_callbacks[0]() - self._connection_callbacks.shift() - } - }) -} - - - - - - -Remit.prototype.__use_consume_channel = function __use_consume_channel (callback) { - const self = this - - if (!callback) { - callback = function () {} - } - - if (self._consume_channel) { - if (self._consume_channel_callbacks.length) { - self._consume_channel_callbacks.push(callback) - - return - } - - return callback() - } - - const first = !self._consume_channel_callbacks.length - self._consume_channel_callbacks.push(callback) - - if (!first) { - return - } - - self.__connect(() => { - self._connection.createChannel((err, channel) => { - channel.on('error', (err) => { - console.error(err) - self._consume_channel = null - self.__use_consume_channel() - }) - - channel.on('close', () => { - throw new Error('Consumption channel closed') - self._consume_channel = null - self.__use_consume_channel() - }) - - channel.prefetch(self._prefetch) - - self._consume_channel = channel - - // Loop through and make everything happen! - while (self._consume_channel_callbacks.length > 0) { - self._consume_channel_callbacks[0]() - self._consume_channel_callbacks.shift() - } - }) - }) -} - - - - - - -Remit.prototype.__use_publish_channel = function __use_publish_channel (callback) { - const self = this - - if (!callback) { - callback = function () {} - } - - if (self._publish_channel) { - if (self._publish_channel_callbacks.length) { - self._publish_channel_callbacks.push(callback) - - return - } - - return callback() - } - - const first = !self._publish_channel_callbacks.length - self._publish_channel_callbacks.push(callback) - - if (!first) { - return - } - - self.__connect(() => { - self._connection.createChannel((err, channel) => { - channel.on('error', (err) => { - console.error(err) - self._publish_channel = null - self.__use_publish_channel() - }) - - channel.on('close', () => { - throw new Error('Publish channel closed') - self._publish_channel = null - self.__use_publish_channel() - }) - - self._publish_channel = channel - - // Loop through and make everything happen! - while (self._publish_channel_callbacks.length > 0) { - self._publish_channel_callbacks[0]() - self._publish_channel_callbacks.shift() - } - }) - }) -} - - - - - - -Remit.prototype.__assert_exchange = function __assert_exchange (callback) { - const self = this - - // If no callback was given, we still pretend there - // is one. We use this to signify queue presence. - if (!callback) { - callback = function () {} - } - - // If the exchange already exists - if (self._exchange) { - // If there are still callbacks being processed, - // hop into the queue; no need to trigger it now. - // Be British and get in line! - if (self._exchange_callbacks.length) { - self._exchange_callbacks.push(callback) - - return - } - - // Otherwise we do need to trigger now. We missed - // the queue. #awkward - return callback() - } - - // If we're here, an exchange doesn't currently exist. - // Now we check whether we're the first call to do this. - // If we are, we'll be the ones to try and connect. - const first = !self._exchange_callbacks.length - - // Push our callback in to the queue, eh? - self._exchange_callbacks.push(callback) - - if (!first) { - return - } - - // Let's try making this exchange! - self._worker_pool.acquire((err, channel) => { - channel.assertExchange(self._exchange_name, 'topic', { - autoDelete: true - }, (err, ok) => { - if (err) { - throw err - } - - self._worker_pool.release(channel) - - // Everything went awesome so we'll let everything - // know that the exchange is up. - self._exchange = true - - // Time to run any callbacks that were waiting on - // this exchange being made. - // Loop through and make everything happen! - while (self._exchange_callbacks.length > 0) { - self._exchange_callbacks[0]() - self._exchange_callbacks.shift() - } - }) - }) -} - - - - - - -Remit.prototype.__consume_res = function __consume_res (message, callbacks, context, consumerOptions) { - const self = this - - let data - - try { - data = JSON.parse(message.content.toString()) - } catch (e) { - return self.__use_consume_channel(() => { - self._consume_channel.nack(message, false, false) - }) - } - - const extra = { - service: message.properties.appId, - event: message.properties.type, - caller: message.properties.messageId, - timestamp: message.properties.timestamp ? new Date(parseInt(message.properties.timestamp) * 1000) : new Date(), - uuid: message.properties.headers && message.properties.headers.uuid - } - - if (!message.properties.correlationId || !message.properties.replyTo) { - function done (err, data) { - self.__use_consume_channel(() => { - if (!consumerOptions.noAck) self._consume_channel.ack(message) - }) - } - - try { - step_through_callbacks(callbacks, data, extra, done) - } catch (e) { - if (message.properties.headers && message.properties.headers.attempts && message.properties.headers.attempts > 4) { - self.__use_consume_channel(() => { - if (!consumerOptions.noAck) self._consume_channel.nack(message, false, false) - }) - } else { - message.properties.headers = increment_headers(message.properties.headers) - - function check_and_republish() { - self._worker_pool.acquire((err, channel) => { - channel.checkQueue(message.properties.replyTo, (err, ok) => { - if (err) { - self._worker_pool.remove(channel) - - // If we got a proper queue error then the queue must - // just not be around. - if (err.message.substr(0, 16) === 'Operation failed') { - self.__use_consume_channel(() => { - self._consume_channel.nack(message, false, false) - }) - } else { - check_and_republish() - } - } else { - self._worker_pool.release(channel) - - self.__use_publish_channel(() => { - self._publish_channel.publish('', message.properties.replyTo, message.content, message.properties) - }) - - self.__use_consume_channel(() => { - if (!consumerOptions.noAck) self._consume_channel.ack(message) - }) - } - }) - }) - } - - check_and_republish() - } - - if (self.on_error) { - self.on_error(e) - } else { - throw e - } - } - } else { - function done (err, data) { - const options = {correlationId: message.properties.correlationId} - const res_data = new Buffer(JSON.stringify(Array.prototype.slice.call(arguments))) - - function check_and_publish () { - self._worker_pool.acquire((err, channel) => { - channel.checkQueue(message.properties.replyTo, (err, ok) => { - if (err) { - self._worker_pool.remove(channel) - - // If we got a proper queue error then the queue must - // just not be around. - if (err.message.substr(0, 16) === 'Operation failed') { - self.__use_consume_channel(() => { - if (!consumerOptions.noAck) self._consume_channel.nack(message, false, false) - }) - } else { - check_and_publish() - } - } else { - self._worker_pool.release(channel) - - self.__use_publish_channel(() => { - self._publish_channel.publish('', message.properties.replyTo, res_data, options) - }) - - self.__use_consume_channel(() => { - if (!consumerOptions.noAck) self._consume_channel.ack(message) - }) - } - }) - }) - } - - check_and_publish() - } - - try { - step_through_callbacks(callbacks, data, extra, done) - } catch (e) { - if (message.properties.headers && message.properties.headers.attempts && message.properties.headers.attempts > 4) { - self.__use_consume_channel(() => { - self._consume_channel.nack(message, false, false) - }) - } else { - message.properties.headers = increment_headers(message.properties.headers) - - function check_and_republish () { - self._worker_pool.acquire((err, channel) => { - channel.checkQueue(message.properties.replyTo, (err, ok) => { - if (err) { - self._worker_pool.remove(channel) - - // If we got a proper queue error then the queue must - // just not be around. - if (err.message.substr(0, 16) === 'Operation failed') { - self.__use_consume_channel(() => { - self._consume_channel.nack(message, false, false) - }) - } else { - check_and_republish() - } - } else { - self._worker_pool.release(channel) - - self.__use_publish_channel(() => { - self._publish_channel.publish('', message.properties.replyTo, message.content, message.properties) - }) - - self.__use_consume_channel(() => { - self._consume_channel.ack(message) - }) - } - }) - }) - } - - check_and_republish() - } - - if (self.on_error) { - self.on_error(e) - } else { - throw e - } - } - } +function remit (options) { + return new Remit(options) } - - - - - -Remit.prototype.__on_result = function __on_result (message) { - const self = this - - const callback = self._results_callbacks[message.properties.correlationId] - - let data = JSON.parse(message.content.toString()) - if (!Array.isArray(data)) data = [data] - - delete self._results_timeouts[message.properties.correlationId] - - // If it turns out we don't have a callback here (this can - // happen if the timeout manages to get there first) then - // let's exit before we get ourselves into trouble. - if (!callback) { - return - } - - try { - callback.callback.apply(callback.context, data) - } catch (e) { - delete self._results_callbacks[message.properties.correlationId] - - if (self.on_error) { - self.on_error(e) - } else { - throw e - } - } - - delete self._results_callbacks[message.properties.correlationId] -} - - - - - - -function increment_headers (headers) { - if (!headers) { - return { - attempts: 1 - } - } - - if (!headers.attempts) { - headers.attempts = 1 - - return headers - } - - headers.attempts = parseInt(headers.attempts) + 1 - - return headers -} - - - - - - -function step_through_callbacks (callbacks, args, extra, done, index) { - args = args !== undefined ? args : {} - extra = extra || {} - - if (!index) { - index = 0 - - if (!Array.isArray(callbacks)) { - return callbacks(args, done, extra) - } - - if (callbacks.length === 1) { - return callbacks[index](args, done, extra) - } - - return callbacks[index](args, (err, args) => { - if (err) { - return done(err, args) - } - - return step_through_callbacks(callbacks, args, extra, done, ++index) - }, extra) - } - - if (!callbacks[index]) { - return done(null, args) - } - - return callbacks[index](args, (err, args) => { - if (err) { - return done(err, args) - } - - return step_through_callbacks(callbacks, args, extra, done, ++index) - }, extra) -} +module.exports = remit diff --git a/lib/Emitter.js b/lib/Emitter.js new file mode 100644 index 0000000..472ab52 --- /dev/null +++ b/lib/Emitter.js @@ -0,0 +1,195 @@ +const CallableInstance = require('callable-instance') +const EventEmitter = require('eventemitter3') +const genUuid = require('../utils/genUuid') +const parseEvent = require('../utils/parseEvent') +const getStackLine = require('../utils/getStackLine') + +class Emitter extends CallableInstance { + constructor (remit, opts = {}) { + super('send') + + this._remit = remit + this._emitter = new EventEmitter() + + let parsedOpts = {} + + if (typeof opts === 'string') { + parsedOpts.event = opts + } else { + parsedOpts = opts + } + + if (!parsedOpts.event) { + throw new Error('No/invalid event specified when creating an emission') + } + + this.options(parsedOpts) + + this._ready = Promise.resolve(this) + } + + on (...args) { + // should we warn/block users when they try + // to listen to an event that doesn't exist? + this._emitter.on(...args) + + return this + } + + options (opts = {}) { + this._options = this._generateOptions(opts) + + return this + } + + ready () { + return this._ready + } + + async send (data = null, opts = {}) { + // parse the callsites here, as after the `await` + // we'll get a different stack + const callsites = getStackLine.capture() + const now = new Date().getTime() + const parsedOptions = this._generateOptions(opts) + + const messageId = genUuid() + + const message = { + mandatory: false, + messageId: messageId, + appId: this._remit._options.name, + timestamp: now, + headers: { + trace: getStackLine.parse(callsites) + } + } + + if (parsedOptions.priority) { + if (parsedOptions.priority > 10 || parsedOptions.priority < 0) { + throw new Error(`Invalid priority "${parsedOptions.priority}" when making request`) + } + + message.priority = parsedOptions.priority + } + + let parsedData + + // coerce data to `null` if undefined or an unparsable pure JS property. + parsedData = JSON.stringify(data) + + if (typeof parsedData === 'undefined') { + console.warn('[WARN] Remit request sent with unparsable JSON; this could be a function or an undefined variable. Data instead set to NULL.') + + // string here coerces to actual NULL once JSON.parse is performed + parsedData = 'null' + } + + const demitQueue = await this._setupDemitQueue(parsedOptions, now) + const worker = await this._remit._workers.acquire() + + try { + if (demitQueue) { + const { queue, expiration } = demitQueue + + if (parsedOptions.schedule) { + message.headers.scheduled = +parsedOptions.schedule + message.expiration = expiration + } else { + message.headers.delay = parsedOptions.delay + } + + worker.sendToQueue( + queue, + Buffer.from(parsedData), + message + ) + } else { + worker.publish( + this._remit._exchange, + parsedOptions.event, + Buffer.from(parsedData), + message + ) + } + + this._remit._workers.release(worker) + + // We do this to make room for multiple emits. + // without this, continued synchronous emissions + // never get a chance to send + await new Promise(resolve => setImmediate(resolve)) + + const event = parseEvent(message, { + routingKey: parsedOptions.event + }, JSON.parse(parsedData), true) + + this._emitter.emit('sent', event) + + return event + } catch (e) { + this._remit._workers.destroy(worker) + throw e + } + } + + _generateOptions (opts = {}) { + return Object.assign({}, this._options || {}, opts) + } + + async _setupDemitQueue (opts, time) { + if (isNaN(opts.delay) && !opts.schedule) { + return false + } + + if ( + (!opts.delay || isNaN(opts.delay)) && + (!opts.schedule || !(opts.schedule instanceof Date) || opts.schedule.toString() === 'Invalid Date') + ) { + throw new Error('Invalid schedule date or delay duration when attempting to send a delayed emission') + } + + const group = opts.schedule ? +opts.schedule : opts.delay + const expiration = opts.schedule ? (+opts.schedule - time) : opts.delay + + if (expiration < 1) { + return false + } + + const queueOpts = { + exclusive: false, + durable: true, + autoDelete: true, + deadLetterExchange: this._remit._exchange, + deadLetterRoutingKey: opts.event + } + + if (opts.delay) { + queueOpts.messageTtl = expiration + queueOpts.expires = expiration * 2 + } else { + queueOpts.expires = expiration + 60000 + } + + const worker = await this._remit._workers.acquire() + const queue = `d:${this._remit._exchange}:${opts.event}:${group}` + + try { + await worker.assertQueue(queue, queueOpts) + this._remit._workers.release(worker) + return { queue, expiration } + } catch (e) { + this._remit._workers.destroy(worker) + + // if we're scheduling an emission and we have an inequivalent + // x-expires argument, that's fine; that'll happen + if (opts.schedule && e.message && e.message.substr(94, 28) === 'inequivalent arg \'x-expires\'') { + return { queue, expiration } + } else { + throw e + } + } + } +} + +module.exports = Emitter diff --git a/lib/Endpoint.js b/lib/Endpoint.js new file mode 100644 index 0000000..deb3f8e --- /dev/null +++ b/lib/Endpoint.js @@ -0,0 +1,160 @@ +const EventEmitter = require('eventemitter3') +const parseEvent = require('../utils/parseEvent') +const waterfall = require('../utils/asyncWaterfall') +const serializeData = require('../utils/serializeData') +const handlerWrapper = require('../utils/handlerWrapper') + +class Endpoint { + constructor (remit, opts, ...handlers) { + this._remit = remit + this._emitter = new EventEmitter() + + let parsedOpts = {} + + if (typeof opts === 'string') { + parsedOpts.event = opts + } else { + parsedOpts = opts || {} + } + + if (!parsedOpts.event) { + throw new Error('No/invalid event specified when creating an endpoint') + } + + this.options(parsedOpts) + + if (handlers.length) { + this.handler(...handlers) + } + } + + handler (...fns) { + if (!fns.length) { + throw new Error('No handler(s) given when trying to set endpoint handler(s)') + } + + this._handler = waterfall(...fns.map(handlerWrapper)) + + return this + } + + on (...args) { + // should we warn/block users when they try + // to listen to an event that doesn't exist? + this._emitter.on(...args) + + return this + } + + options (opts = {}) { + opts.queue = opts.queue || opts.event || this._options.queue || this._options.event + this._options = Object.assign({}, this._options || {}, opts) + + return this + } + + // TODO should we emit something once booted? + start () { + if (this._started) { + return this._started + } + + if (!this._handler) { + throw new Error('Trying to boot endpoint with no handler') + } + + this._started = this._setup(this._options) + + return this._started + } + + async _incoming (message) { + if (!message) { + throw new Error('Consumer cancelled unexpectedly; this was most probably done via RabbitMQ\'s management panel') + } + + try { + var data = JSON.parse(message.content.toString()) + } catch (e) { + // if this fails, there's no need to nack, + // so just ignore it + return + } + + const event = parseEvent(message.properties, message.fields, data) + const resultOp = this._handler(event) + + try { + this._emitter.emit('data', event) + } catch (e) { + console.error(e) + } + + const canReply = Boolean(message.properties.replyTo) + + if (!canReply) { + await resultOp + } else { + let finalData = await resultOp + finalData = serializeData(finalData) + + const worker = await this + ._remit + ._workers + .acquire() + + try { + await worker.sendToQueue( + message.properties.replyTo, + Buffer.from(finalData), + message.properties + ) + + this._remit._workers.release(worker) + } catch (e) { + this._remit._workers.destroy(worker) + } + } + } + + async _setup ({ queue, event }) { + const worker = await this._remit._workers.acquire() + + try { + await worker.assertQueue(queue, { + exclusive: false, + durable: true, + autoDelete: false, + maxPriority: 10 + }) + + this._remit._workers.release(worker) + } catch (e) { + this._remit._workers.destroy(worker) + throw e + } + + const connection = await this._remit._connection + this._consumer = await connection.createChannel() + this._consumer.prefetch(48) + + await this._consumer.bindQueue( + queue, + this._remit._exchange, + event + ) + + await this._consumer.consume( + queue, + this._incoming.bind(this), + { + noAck: true, + exclusive: false + } + ) + + return this + } +} + +module.exports = Endpoint diff --git a/lib/Listener.js b/lib/Listener.js new file mode 100644 index 0000000..583f069 --- /dev/null +++ b/lib/Listener.js @@ -0,0 +1,136 @@ +const EventEmitter = require('eventemitter3') +const parseEvent = require('../utils/parseEvent') +const waterfall = require('../utils/asyncWaterfall') +const handlerWrapper = require('../utils/handlerWrapper') + +class Listener { + constructor (remit, opts, ...handlers) { + this._remit = remit + this._emitter = new EventEmitter() + + let parsedOpts = {} + + if (typeof opts === 'string') { + parsedOpts.event = opts + } else { + parsedOpts = opts + } + + if (!parsedOpts.event) { + throw new Error('No/invalid event specified when creating an endpoint') + } + + this.options(parsedOpts) + + if (handlers.length) { + this.handler(...handlers) + } + } + + handler (...fns) { + this._handler = waterfall(...fns.map(handlerWrapper)) + + return this + } + + on (...args) { + // should we warn/block users when they try + // to listen to an event that doesn't exist? + this._emitter.on(...args) + + return this + } + + options (opts = {}) { + const event = opts.event || this._options.event + this._remit._eventCounters[event] = this._remit._eventCounters[event] || 0 + + opts.queue = opts.queue || `${opts.event || this._options.event}:l:${this._remit._options.name}:${++this._remit._eventCounters[event]}` + + this._options = Object.assign({}, this._options || {}, opts) + + return this + } + + start () { + if (this._started) { + return this._started + } + + if (!this._handler) { + throw new Error('Trying to boot listener with no handler') + } + + this._started = this._setup(this._options) + + return this._started + } + + async _incoming (message) { + if (!message) { + throw new Error('Consumer cancelled unexpectedly; this was most probably done via RabbitMQ\'s management panel') + } + + try { + var data = JSON.parse(message.content.toString()) + } catch (e) { + // if this fails, let's just nack the message and leave + this._consumer.nack(message) + + return + } + + const event = parseEvent(message.properties, message.fields, data) + const resultOp = this._handler(event) + + try { + this._emitter.emit('data', event) + } catch (e) { + console.error(e) + } + + await resultOp + this._consumer.ack(message) + } + + async _setup ({ queue, event }) { + const worker = await this._remit._workers.acquire() + + try { + await worker.assertQueue(queue, { + exclusive: false, + durable: true, + autoDelete: false, + maxPriority: 10 + }) + + this._remit._workers.release(worker) + } catch (e) { + this._remit._workers.destroy(worker) + throw e + } + + const connection = await this._remit._connection + this._consumer = await connection.createChannel() + this._consumer.prefetch(48) + + await this._consumer.bindQueue( + queue, + this._remit._exchange, + event + ) + + await this._consumer.consume( + queue, + this._incoming.bind(this), + { + noAck: false, + exclusive: false + } + ) + + return this + } +} + +module.exports = Listener diff --git a/lib/Remit.js b/lib/Remit.js new file mode 100644 index 0000000..acab976 --- /dev/null +++ b/lib/Remit.js @@ -0,0 +1,66 @@ +const amqplib = require('amqplib') +const EventEmitter = require('eventemitter3') +const packageJson = require('../package.json') +const parseAmqpUrl = require('../utils/parseAmqpUrl') +const generateConnectionOptions = require('../utils/generateConnectionOptions') +const ChannelPool = require('../utils/ChannelPool') +const CallableWrapper = require('../utils/CallableWrapper') +const Endpoint = require('./Endpoint') +const Listener = require('./Listener') +const Request = require('./Request') +const Emitter = require('./Emitter') + +class Remit { + constructor (options = {}) { + this.listen = new CallableWrapper(this, Listener) + this.emit = new CallableWrapper(this, Emitter) + this.endpoint = new CallableWrapper(this, Endpoint) + this.request = new CallableWrapper(this, Request) + + this.version = packageJson.version + + this._options = {} + + this._options.exchange = options.exchange || 'remit' + this._options.name = options.name || process.env.REMIT_NAME || '' + this._options.url = options.url || process.env.REMIT_URL || 'amqp://localhost' + + this._emitter = new EventEmitter() + this._connection = this._connect(this._options) + this._workers = ChannelPool(this._connection) + + // TODO make this better + this._eventCounters = {} + } + + on (...args) { + this._emitter.on(...args) + } + + async _connect ({ url, name, exchange }) { + const amqpUrl = parseAmqpUrl(url) + const connectionOptions = generateConnectionOptions(name) + const connection = await amqplib.connect(amqpUrl, connectionOptions) + + const tempChannel = await connection.createChannel() + + await tempChannel.assertExchange(exchange, 'topic', { + durable: true, + internal: false, + autoDelete: true + }) + + tempChannel.close() + + return connection + } + + // Should we expose `name`, `exchange` and `url` publically? + // we can use getters so they're still actually saved within + // _options, but exposing them might be cool. + get _exchange () { + return this._options.exchange + } +} + +module.exports = Remit diff --git a/lib/Request.js b/lib/Request.js new file mode 100644 index 0000000..254f61a --- /dev/null +++ b/lib/Request.js @@ -0,0 +1,200 @@ +const CallableInstance = require('callable-instance') +const EventEmitter = require('eventemitter3') +const genUuid = require('../utils/genUuid') +const parseEvent = require('../utils/parseEvent') +const getStackLine = require('../utils/getStackLine') + +class Request extends CallableInstance { + constructor (remit, opts = {}) { + super('send') + + this._remit = remit + this._emitter = new EventEmitter() + this._timers = {} + + let parsedOpts = {} + + if (typeof opts === 'string') { + parsedOpts.event = opts + } else { + parsedOpts = opts + } + + if (!parsedOpts.event) { + throw new Error('No/invalid event specified when creating a request') + } + + this.options(parsedOpts) + + this._ready = this._setup(this._options) + } + + on (...args) { + // should we warn/block users when they try + // to listen to an event that doesn't exist? + this._emitter.on(...args) + + return this + } + + fallback (data) { + this._fallback = data + + return this + } + + options (opts = {}) { + this._options = this._generateOptions(opts) + + return this + } + + ready () { + return this._ready + } + + async send (data = null, opts = {}) { + // parse the callsites here, as after the `await` + // we'll get a different stack + const callsites = getStackLine.capture() + await this._ready + const now = new Date().getTime() + const parsedOptions = this._generateOptions(opts) + + const messageId = genUuid() + + const message = { + mandatory: false, + messageId: messageId, + appId: this._remit._options.name, + timestamp: now, + headers: { + trace: getStackLine.parse(callsites) + }, + correlationId: messageId, + replyTo: 'amq.rabbitmq.reply-to' + } + + if (parsedOptions.priority) { + if (parsedOptions.priority > 10 || parsedOptions.priority < 0) { + throw new Error(`Invalid priority "${parsedOptions.priority}" when making request`) + } + + message.priority = parsedOptions.priority + } + + let parsedData + let eventData = data + + // coerce data to `null` if undefined or an unparsable pure JS property. + parsedData = JSON.stringify(data) + + if (typeof parsedData === 'undefined') { + console.warn('[WARN] Remit request sent with unparsable JSON; this could be a function or an undefined variable. Data instead set to NULL.') + + // string here coerces to actual NULL once JSON.parse is performed + parsedData = 'null' + eventData = null + } + + this._channel.publish( + this._remit._exchange, + parsedOptions.event, + Buffer.from(parsedData), + message + ) + + const event = parseEvent(message, { + routingKey: parsedOptions.event + }, eventData, true) + + this._emitter.emit('sent', event) + + let timeout = 30000 + let givenTimeout = Number(parsedOptions.timeout) + if (!isNaN(givenTimeout)) timeout = givenTimeout + + if (timeout) { + this._setTimer(messageId, timeout, event) + } + + return this._waitForResult(messageId) + } + + _generateOptions (opts = {}) { + return Object.assign({}, this._options || {}, opts) + } + + async _incoming (message) { + try { + var content = JSON.parse(message.content.toString()) + } catch (e) { + console.error(e) + } + + this._emitter.emit(`data-${message.properties.correlationId}`, ...content) + } + + _setTimer (messageId, time, event) { + this._timers[messageId] = setTimeout(() => { + this._emitter.emit(`timeout-${messageId}`, { + event: event, + code: 'request_timedout', + message: `Request timed out after no response for ${time}ms` + }) + }, time) + } + + async _setup (opts = {}) { + const connection = await this._remit._connection + this._channel = await connection.createChannel() + + await this._channel.consume( + 'amq.rabbitmq.reply-to', + this._incoming.bind(this), + { + noAck: true, + exclusive: true + } + ) + + return this + } + + _waitForResult (messageId) { + const types = ['data', 'timeout'] + + return new Promise((resolve, reject) => { + const cleanUp = (err, result) => { + clearTimeout(this._timers[messageId]) + delete this._timers[messageId] + + types.forEach((type) => { + this._emitter.removeAllListeners(`${type}-${messageId}`) + }) + + if (err) { + this._emitter.emit('error', err) + + if (typeof this._fallback !== 'undefined') { + resolve(this._fallback) + } else { + reject(err) + } + } else { + resolve(result) + this._emitter.emit('success', result) + } + } + + types.forEach((type) => { + this._emitter.once(`${type}-${messageId}`, (...args) => { + cleanUp(...args) + this._emitter.emit(type, ...args) + }) + }) + }) + } +} + +module.exports = Request diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..89b35f6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2603 @@ +{ + "name": "remit", + "version": "2.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "amqplib": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.5.1.tgz", + "integrity": "sha1-fMz+ur5WwumE6noiQ/fO/m+/xs8=", + "requires": { + "bitsyntax": "0.0.4", + "bluebird": "3.5.0", + "buffer-more-ints": "0.0.2", + "readable-stream": "1.1.14" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true + }, + "assertion-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", + "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "bitsyntax": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/bitsyntax/-/bitsyntax-0.0.4.tgz", + "integrity": "sha1-6xDMb4K4xJDj6FaY8H6D1G4MuoI=", + "requires": { + "buffer-more-ints": "0.0.2" + } + }, + "bluebird": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", + "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "buffer-more-ints": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-0.0.2.tgz", + "integrity": "sha1-JrOIXRD6E9t/wBquOquHAZngEkw=" + }, + "callable-instance": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callable-instance/-/callable-instance-1.0.0.tgz", + "integrity": "sha1-1sfKqsr8WYwC1E4sbiD6rpY0VJs=" + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", + "dev": true + }, + "chai": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", + "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "dev": true, + "requires": { + "assertion-error": "1.0.2", + "check-error": "1.0.2", + "deep-eql": "3.0.0", + "get-func-name": "2.0.0", + "pathval": "1.1.0", + "type-detect": "4.0.3" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "coveralls": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-2.13.1.tgz", + "integrity": "sha1-1wu5rMGDXsTwY/+drFQjwXsR8Xg=", + "dev": true, + "requires": { + "js-yaml": "3.6.1", + "lcov-parse": "0.0.10", + "log-driver": "1.2.5", + "minimist": "1.2.0", + "request": "2.79.0" + } + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-eql": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.0.tgz", + "integrity": "sha512-9zef2MtjASSE1Pts2Nm6Yh5MTVdVh+s4Qt/e+jPV6qTBhqTc0WOEaWnLvLKGxky0gwZGmcY6TnUqyCD6fNs5Lg==", + "dev": true, + "requires": { + "type-detect": "4.0.3" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "diff": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", + "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=" + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "1.0.2" + } + }, + "generic-pool": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.1.7.tgz", + "integrity": "sha1-2sIrLHp6BOQXMvfY0tJaMDyI9mI=" + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "commander": "2.11.0", + "is-my-json-valid": "2.16.1", + "pinkie-promise": "2.0.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "is-my-json-valid": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz", + "integrity": "sha512-ochPsqWS1WXj8ZnMIV0vnNXooaMhp7cyL4FMSIPKTtnV0Ha/T19G2b9kkhcNsabV9bxYkze7/aLZJb/bYuFduQ==", + "dev": true, + "requires": { + "generate-function": "2.0.0", + "generate-object-property": "1.2.0", + "jsonpointer": "4.0.1", + "xtend": "4.0.1" + } + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "js-yaml": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", + "integrity": "sha1-bl/mfYsgXOTSL60Ft3geja3MSzA=", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "2.7.3" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "lcov-parse": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", + "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", + "dev": true + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "3.0.1", + "lodash.keys": "3.1.2" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basecreate": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", + "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash.create": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", + "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "dev": true, + "requires": { + "lodash._baseassign": "3.2.0", + "lodash._basecreate": "3.0.3", + "lodash._isiterateecall": "3.0.9" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.1.0", + "lodash.isarray": "3.0.4" + } + }, + "log-driver": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz", + "integrity": "sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY=", + "dev": true + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=", + "dev": true + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "dev": true, + "requires": { + "mime-db": "1.30.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "mocha": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.0.tgz", + "integrity": "sha512-pIU2PJjrPYvYRqVpjXzj76qltO9uBYI7woYAMoxbSefsa+vqAfptjoeevd6bUgwD0mPIO+hv9f7ltvsNreL2PA==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.9.0", + "debug": "2.6.8", + "diff": "3.2.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.1", + "growl": "1.9.2", + "json3": "3.3.2", + "lodash.create": "3.1.1", + "mkdirp": "0.5.1", + "supports-color": "3.1.2" + }, + "dependencies": { + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "requires": { + "graceful-readlink": "1.0.1" + } + }, + "supports-color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", + "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "nyc": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-11.1.0.tgz", + "integrity": "sha1-1rPF4WiSolr2MTi6SEZ2qooi7ac=", + "dev": true, + "requires": { + "archy": "1.0.0", + "arrify": "1.0.1", + "caching-transform": "1.0.1", + "convert-source-map": "1.5.0", + "debug-log": "1.0.1", + "default-require-extensions": "1.0.0", + "find-cache-dir": "0.1.1", + "find-up": "2.1.0", + "foreground-child": "1.5.6", + "glob": "7.1.2", + "istanbul-lib-coverage": "1.1.1", + "istanbul-lib-hook": "1.0.7", + "istanbul-lib-instrument": "1.7.4", + "istanbul-lib-report": "1.1.1", + "istanbul-lib-source-maps": "1.2.1", + "istanbul-reports": "1.1.1", + "md5-hex": "1.3.0", + "merge-source-map": "1.0.4", + "micromatch": "2.3.11", + "mkdirp": "0.5.1", + "resolve-from": "2.0.0", + "rimraf": "2.6.1", + "signal-exit": "3.0.2", + "spawn-wrap": "1.3.8", + "test-exclude": "4.1.1", + "yargs": "8.0.2", + "yargs-parser": "5.0.0" + }, + "dependencies": { + "align-text": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "amdefine": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "bundled": true, + "dev": true + }, + "append-transform": { + "version": "0.4.0", + "bundled": true, + "dev": true, + "requires": { + "default-require-extensions": "1.0.0" + } + }, + "archy": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "arr-diff": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "arrify": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "async": { + "version": "1.5.2", + "bundled": true, + "dev": true + }, + "babel-code-frame": { + "version": "6.22.0", + "bundled": true, + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "babel-generator": { + "version": "6.25.0", + "bundled": true, + "dev": true, + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.23.0", + "babel-types": "6.25.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.4", + "source-map": "0.5.6", + "trim-right": "1.0.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "6.23.0" + } + }, + "babel-runtime": { + "version": "6.23.0", + "bundled": true, + "dev": true, + "requires": { + "core-js": "2.4.1", + "regenerator-runtime": "0.10.5" + } + }, + "babel-template": { + "version": "6.25.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0", + "babylon": "6.17.4", + "lodash": "4.17.4" + } + }, + "babel-traverse": { + "version": "6.25.0", + "bundled": true, + "dev": true, + "requires": { + "babel-code-frame": "6.22.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.23.0", + "babel-types": "6.25.0", + "babylon": "6.17.4", + "debug": "2.6.8", + "globals": "9.18.0", + "invariant": "2.2.2", + "lodash": "4.17.4" + } + }, + "babel-types": { + "version": "6.25.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.17.4", + "bundled": true, + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.8", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "bundled": true, + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "builtin-modules": { + "version": "1.1.1", + "bundled": true, + "dev": true + }, + "caching-transform": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "md5-hex": "1.3.0", + "mkdirp": "0.5.1", + "write-file-atomic": "1.3.4" + } + }, + "camelcase": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true + }, + "center-align": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chalk": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "cliui": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "commondir": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "convert-source-map": { + "version": "1.5.0", + "bundled": true, + "dev": true + }, + "core-js": { + "version": "2.4.1", + "bundled": true, + "dev": true + }, + "cross-spawn": { + "version": "4.0.2", + "bundled": true, + "dev": true, + "requires": { + "lru-cache": "4.1.1", + "which": "1.2.14" + } + }, + "debug": { + "version": "2.6.8", + "bundled": true, + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "debug-log": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "decamelize": { + "version": "1.2.0", + "bundled": true, + "dev": true + }, + "default-require-extensions": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "strip-bom": "2.0.0" + } + }, + "detect-indent": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "error-ex": { + "version": "1.3.1", + "bundled": true, + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "bundled": true, + "dev": true + }, + "esutils": { + "version": "2.0.2", + "bundled": true, + "dev": true + }, + "execa": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "cross-spawn": "4.0.2", + "get-stream": "2.3.1", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } + }, + "expand-brackets": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "bundled": true, + "dev": true, + "requires": { + "fill-range": "2.2.3" + } + }, + "extglob": { + "version": "0.3.2", + "bundled": true, + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "filename-regex": { + "version": "2.0.1", + "bundled": true, + "dev": true + }, + "fill-range": { + "version": "2.2.3", + "bundled": true, + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "find-cache-dir": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "requires": { + "commondir": "1.0.1", + "mkdirp": "0.5.1", + "pkg-dir": "1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "for-own": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "foreground-child": { + "version": "1.5.6", + "bundled": true, + "dev": true, + "requires": { + "cross-spawn": "4.0.2", + "signal-exit": "3.0.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "get-caller-file": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "get-stream": { + "version": "2.3.1", + "bundled": true, + "dev": true, + "requires": { + "object-assign": "4.1.1", + "pinkie-promise": "2.0.1" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "bundled": true, + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "globals": { + "version": "9.18.0", + "bundled": true, + "dev": true + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true, + "dev": true + }, + "handlebars": { + "version": "4.0.10", + "bundled": true, + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "bundled": true, + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "has-ansi": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "hosted-git-info": { + "version": "2.5.0", + "bundled": true, + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "bundled": true, + "dev": true + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "invariant": { + "version": "2.2.2", + "bundled": true, + "dev": true, + "requires": { + "loose-envify": "1.3.1" + } + }, + "invert-kv": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "is-buffer": { + "version": "1.1.5", + "bundled": true, + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-dotfile": { + "version": "1.0.3", + "bundled": true, + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-glob": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-number": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "isexe": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "isobject": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "istanbul-lib-coverage": { + "version": "1.1.1", + "bundled": true, + "dev": true + }, + "istanbul-lib-hook": { + "version": "1.0.7", + "bundled": true, + "dev": true, + "requires": { + "append-transform": "0.4.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.7.4", + "bundled": true, + "dev": true, + "requires": { + "babel-generator": "6.25.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0", + "babylon": "6.17.4", + "istanbul-lib-coverage": "1.1.1", + "semver": "5.3.0" + } + }, + "istanbul-lib-report": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "requires": { + "istanbul-lib-coverage": "1.1.1", + "mkdirp": "0.5.1", + "path-parse": "1.0.5", + "supports-color": "3.2.3" + }, + "dependencies": { + "supports-color": { + "version": "3.2.3", + "bundled": true, + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "requires": { + "debug": "2.6.8", + "istanbul-lib-coverage": "1.1.1", + "mkdirp": "0.5.1", + "rimraf": "2.6.1", + "source-map": "0.5.6" + } + }, + "istanbul-reports": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "requires": { + "handlebars": "4.0.10" + } + }, + "js-tokens": { + "version": "3.0.2", + "bundled": true, + "dev": true + }, + "jsesc": { + "version": "1.3.0", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "bundled": true, + "dev": true, + "optional": true + }, + "lcid": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "invert-kv": "1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "bundled": true, + "dev": true + } + } + }, + "lodash": { + "version": "4.17.4", + "bundled": true, + "dev": true + }, + "longest": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "bundled": true, + "dev": true, + "requires": { + "js-tokens": "3.0.2" + } + }, + "lru-cache": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "md5-hex": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "md5-o-matic": "0.1.1" + } + }, + "md5-o-matic": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "mem": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "mimic-fn": "1.1.0" + } + }, + "merge-source-map": { + "version": "1.0.4", + "bundled": true, + "dev": true, + "requires": { + "source-map": "0.5.6" + } + }, + "micromatch": { + "version": "2.3.11", + "bundled": true, + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.3" + } + }, + "mimic-fn": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "normalize-package-data": { + "version": "2.4.0", + "bundled": true, + "dev": true, + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.3.0", + "validate-npm-package-license": "3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "remove-trailing-separator": "1.0.2" + } + }, + "npm-run-path": { + "version": "2.0.2", + "bundled": true, + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true + }, + "object.omit": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "optimist": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8", + "wordwrap": "0.0.3" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "os-locale": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "execa": "0.5.1", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "p-finally": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "p-limit": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "p-locate": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "p-limit": "1.1.0" + } + }, + "parse-glob": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parse-json": { + "version": "2.2.0", + "bundled": true, + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "path-exists": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "path-key": { + "version": "2.0.1", + "bundled": true, + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "bundled": true, + "dev": true + }, + "path-type": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pify": { + "version": "2.3.0", + "bundled": true, + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "bundled": true, + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-dir": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "find-up": "1.1.2" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + } + } + }, + "preserve": { + "version": "0.2.0", + "bundled": true, + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "randomatic": { + "version": "1.1.7", + "bundled": true, + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "read-pkg": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + } + } + }, + "regenerator-runtime": { + "version": "0.10.5", + "bundled": true, + "dev": true + }, + "regex-cache": { + "version": "0.4.3", + "bundled": true, + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3", + "is-primitive": "2.0.0" + } + }, + "remove-trailing-separator": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "bundled": true, + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "bundled": true, + "dev": true + }, + "repeating": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "require-directory": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "resolve-from": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "right-align": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.1", + "bundled": true, + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "semver": { + "version": "5.3.0", + "bundled": true, + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true + }, + "slide": { + "version": "1.1.6", + "bundled": true, + "dev": true + }, + "source-map": { + "version": "0.5.6", + "bundled": true, + "dev": true + }, + "spawn-wrap": { + "version": "1.3.8", + "bundled": true, + "dev": true, + "requires": { + "foreground-child": "1.5.6", + "mkdirp": "0.5.1", + "os-homedir": "1.0.2", + "rimraf": "2.6.1", + "signal-exit": "3.0.2", + "which": "1.2.14" + } + }, + "spdx-correct": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "bundled": true, + "dev": true + }, + "spdx-license-ids": { + "version": "1.2.2", + "bundled": true, + "dev": true + }, + "string-width": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "test-exclude": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "requires": { + "arrify": "1.0.1", + "micromatch": "2.3.11", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "require-main-filename": "1.0.1" + } + }, + "to-fast-properties": { + "version": "1.0.3", + "bundled": true, + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "source-map": "0.5.6", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "yargs": { + "version": "3.10.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "validate-npm-package-license": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "which": { + "version": "1.2.14", + "bundled": true, + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "window-size": { + "version": "0.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "0.0.3", + "bundled": true, + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "write-file-atomic": { + "version": "1.3.4", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "slide": "1.1.6" + } + }, + "y18n": { + "version": "3.2.1", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "2.1.2", + "bundled": true, + "dev": true + }, + "yargs": { + "version": "8.0.2", + "bundled": true, + "dev": true, + "requires": { + "camelcase": "4.1.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "2.0.0", + "read-pkg-up": "2.0.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.0", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "7.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "bundled": true, + "dev": true + }, + "cliui": { + "version": "3.2.0", + "bundled": true, + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "load-json-file": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "strip-bom": "3.0.0" + } + }, + "path-type": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "pify": "2.3.0" + } + }, + "read-pkg": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "load-json-file": "2.0.0", + "normalize-package-data": "2.4.0", + "path-type": "2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "find-up": "2.1.0", + "read-pkg": "2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "yargs-parser": { + "version": "7.0.0", + "bundled": true, + "dev": true, + "requires": { + "camelcase": "4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "5.0.0", + "bundled": true, + "dev": true, + "requires": { + "camelcase": "3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "bundled": true, + "dev": true + } + } + } + } + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "qs": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", + "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", + "dev": true + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "request": { + "version": "2.79.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", + "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", + "dev": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.11.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "2.0.6", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "qs": "6.3.2", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.4.3", + "uuid": "3.1.0" + } + }, + "serialize-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", + "integrity": "sha1-ULZ51WNc34Rme9yOWa9OW4HV9go=" + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "dev": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "tough-cookie": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", + "dev": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", + "dev": true + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "type-detect": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz", + "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo=", + "dev": true + }, + "ulid": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-0.2.0.tgz", + "integrity": "sha1-9Dz1XHgHrsMNZpXLO/6TsuMLYaQ=" + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + } + } +} diff --git a/package.json b/package.json index 0874b1b..85a8f74 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,30 @@ { "name": "remit", - "version": "1.9.2", + "version": "2.0.0", "description": "A small set of functionality used to create microservices that don't need to be aware of one-another's existence.", "main": "index.js", + "engines": { + "node": ">=6" + }, "scripts": { - "test": "./node_modules/.bin/mocha ./test/index.js -w --debug", - "coverage": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- ./test/index.js --timeout=60000" + "coverage": "./node_modules/.bin/nyc ./node_modules/.bin/mocha --require test/bootstrap test/*.test.js test/**/*.test.js && ./node_modules/.bin/nyc report --reporter=lcov", + "test": "./node_modules/.bin/mocha --require test/bootstrap test/*.test.js test/**/*.test.js", + "travis": "./node_modules/.bin/nyc ./node_modules/.bin/mocha --require test/bootstrap test/*.test.js test/**/*.test.js && ./node_modules/.bin/nyc report --reporter=lcovonly && cat ./coverage/lcov.info | ./node_modules/.bin/coveralls && rm -rf ./coverage" }, - "author": "Jack Williams ", - "license": "ISC", + "author": "Jack Williams ", + "license": "MIT", "dependencies": { "amqplib": "^0.5.1", - "debug": "^2.6.8", - "pool2": "^1.4.1", - "stack-trace": "0.0.9", - "uuid": "^3.0.1" - }, - "devDependencies": { - "chai": "^3.5.0", - "chalk": "^1.1.3", - "istanbul": "^0.4.5", - "mocha": "^2.5.3", - "prompt": "^1.0.0" + "callable-instance": "^1.0.0", + "callsite": "^1.0.0", + "eventemitter3": "^2.0.3", + "generic-pool": "^3.1.7", + "serialize-error": "^2.1.0", + "ulid": "^0.2.0" }, "repository": { "type": "git", - "url": "git+ssh://git@github.com/jpwilliams/remit.git" + "url": "https://github.com/jpwilliams/remit.git" }, "keywords": [ "micro", @@ -39,14 +38,24 @@ "request", "response", "emit", - "listen" + "listen", + "distributed", + "events", + "messaging" ], "bugs": { "url": "https://github.com/jpwilliams/remit/issues" }, "homepage": "https://github.com/jpwilliams/remit#readme", "files": [ - "index.js", - "test" - ] + "test", + "lib", + "utils" + ], + "devDependencies": { + "chai": "^4.1.1", + "coveralls": "^2.13.1", + "mocha": "^3.5.0", + "nyc": "^11.1.0" + } } diff --git a/test/bootstrap.js b/test/bootstrap.js new file mode 100644 index 0000000..af15a5c --- /dev/null +++ b/test/bootstrap.js @@ -0,0 +1,3 @@ +const chai = require('chai') + +global.expect = chai.expect diff --git a/test/emitter.test.js b/test/emitter.test.js new file mode 100644 index 0000000..9d10ad3 --- /dev/null +++ b/test/emitter.test.js @@ -0,0 +1,209 @@ +/* global describe, it, before, expect */ +const Remit = require('../') + +describe('Emitter', function () { + describe('#object', function () { + let remit + + before(function () { + remit = Remit() + }) + + it('should be a function', function () { + expect(remit.emit).to.be.a('function') + }) + + it('should expose "on" global function', function () { + expect(remit.emit.on).to.be.a('function') + }) + }) + + describe('#return', function () { + let remit, emitter + + before(function () { + remit = Remit() + emitter = remit.emit('foo') + }) + + it('should throw if no event given', function () { + expect(remit.emit.bind(null)).to.throw('No/invalid event specified when creating an emission') + }) + + it('should return an Emitter', function () { + expect(emitter).to.be.an.instanceof(remit.emit.Type) + }) + + it('should be runnable (#send)', function () { + expect(emitter).to.be.a('function') + }) + + it('should expose an "on" function', function () { + expect(emitter.on).to.be.a('function') + }) + + it('should expose an "options" function', function () { + expect(emitter.options).to.be.a('function') + }) + + it('should expose a "ready" function', function () { + expect(emitter.ready).to.be.a('function') + }) + + it('should expose a "send" function', function () { + expect(emitter.send).to.be.a('function') + }) + }) + + // for this section, assume all other parts of the library + // work and only test emission features + describe('#usage', function (done) { + let listenRemit1, listenRemit2, emitRemit + + before(async function () { + const remit1 = Remit({name: 'listen1'}) + const remit2 = Remit({name: 'listen2'}) + listenRemit1 = remit1.listen('emit-usage').handler(() => {}).start() + listenRemit2 = remit2.listen('emit-usage').handler(() => {}).start() + emitRemit = Remit({name: 'emitRemit'}) + + const [ r1, r2 ] = await Promise.all([listenRemit1, listenRemit2]) + + listenRemit1 = r1 + listenRemit2 = r2 + }) + + it('should return promise on send that resolves on sent') + it('should emit "sent" on sending') + it('should add priority if given in options before send') + it('should add priority if given in options at send') + it('should only set options at send for one emission') + it('should pass `null` as data if JSON unparsable') + it('should throw if demission queue dies before sending') + it('should throw if failing to set up demission queue') + it('should throw if invalid delay given') + it('should throw if invalid schedule given') + it('should not set delay if less than 1ms') + it('should not schedule if less than 1ms') + + it('should emit to all listeners', async function () { + const op = Promise.all([ + waitForNext(listenRemit1), + waitForNext(listenRemit2) + ]) + + const sentEvent = await emitRemit + .emit('emit-usage') + .send({foo: 'bar'}) + + expect(sentEvent).to.have.property('eventType', 'emit-usage') + expect(sentEvent).to.not.have.property('started') + expect(sentEvent).to.have.property('eventId') + expect(sentEvent).to.have.property('resource', 'emitRemit') + expect(sentEvent).to.have.property('resourceTrace') + expect(sentEvent).to.have.property('timestamp') + expect(sentEvent.data.foo).to.equal('bar') + // TODO test trace + + const events = await op + + expect(events).to.have.lengthOf(2) + + events.forEach((event) => { + expect(event).to.have.property('started') + expect(event.eventId).to.equal(sentEvent.eventId) + expect(+event.timestamp).to.equal(+sentEvent.timestamp) + expect(event.eventType).to.equal(sentEvent.eventType) + expect(event.resource).to.equal(sentEvent.resource) + expect(event.resourceTrace).to.equal(sentEvent.resourceTrace) + }) + }) + + it('should delay message by 1 seconds', async function () { + this.slow(3000) + + const op = Promise.all([ + waitForNext(listenRemit1), + waitForNext(listenRemit2) + ]) + + const sentEvent = await emitRemit + .emit('emit-usage') + .options({delay: 1000}) + .send({bar: 'baz'}) + + expect(sentEvent).to.have.property('eventId') + expect(sentEvent).to.not.have.property('started') + expect(sentEvent).to.have.property('eventType', 'emit-usage') + expect(sentEvent).to.have.property('resource', 'emitRemit') + expect(sentEvent.data).to.have.property('bar', 'baz') + expect(sentEvent).to.have.property('delay', 1000) + expect(sentEvent).to.have.property('resourceTrace') + expect(sentEvent).to.have.property('timestamp') + + const events = await op + + events.forEach((event) => { + expect(event).to.have.property('started') + expect(event.delay).to.equal(sentEvent.delay) + expect(+event.timestamp).to.equal(+sentEvent.timestamp) + expect(+event.started).to.be.above(+sentEvent.timestamp + sentEvent.delay) + expect(event.eventId).to.equal(sentEvent.eventId) + expect(event.eventType).to.equal(sentEvent.eventType) + expect(event.resource).to.equal(sentEvent.resource) + expect(event.data.bar).to.equal(sentEvent.data.bar) + expect(event.resourceTrace).to.equal(sentEvent.resourceTrace) + }) + }) + + it('should schedule message for 2 seconds', async function () { + this.timeout(5000) + this.slow(5000) + + const op = Promise.all([ + waitForNext(listenRemit1), + waitForNext(listenRemit2) + ]) + + let d = new Date() + d.setSeconds(d.getSeconds() + 2) + + const sentEvent = await emitRemit + .emit('emit-usage') + .options({schedule: d}) + .send({bar: 'baz'}) + + expect(sentEvent).to.have.property('eventId') + expect(sentEvent).to.not.have.property('started') + expect(sentEvent).to.have.property('eventType', 'emit-usage') + expect(sentEvent).to.have.property('resource', 'emitRemit') + expect(sentEvent.data).to.have.property('bar', 'baz') + expect(sentEvent).to.have.property('scheduled') + expect(+sentEvent.scheduled).to.equal(+d) + expect(sentEvent).to.have.property('resourceTrace') + expect(sentEvent).to.have.property('timestamp') + + const events = await op + + events.forEach((event) => { + expect(event).to.have.property('started') + expect(event.schedule).to.equal(sentEvent.schedule) + expect(+event.timestamp).to.equal(+sentEvent.timestamp) + expect(+event.started).to.be.above(+sentEvent.scheduled) + expect(event.eventId).to.equal(sentEvent.eventId) + expect(event.eventType).to.equal(sentEvent.eventType) + expect(event.resource).to.equal(sentEvent.resource) + expect(event.data.bar).to.equal(sentEvent.data.bar) + expect(event.resourceTrace).to.equal(sentEvent.resourceTrace) + }) + }) + }) +}) + +function waitForNext (instance) { + return new Promise((resolve, reject) => { + instance.on('data', (event) => { + resolve(event) + }) + }) +} diff --git a/test/endpoint.test.js b/test/endpoint.test.js new file mode 100644 index 0000000..ee805f0 --- /dev/null +++ b/test/endpoint.test.js @@ -0,0 +1,420 @@ +/* global describe, it, before, expect */ +const Remit = require('../') + +describe('Endpoint', function () { + let remit + + before(function () { + remit = Remit() + }) + + describe('#object', function () { + it('should be a function', function () { + expect(remit.endpoint).to.be.a('function') + }) + + it('should expose "on" global function', function () { + expect(remit.endpoint.on).to.be.a('function') + }) + }) + + describe('#return', function () { + let remit, endpoint + + before(function () { + remit = Remit() + endpoint = remit.endpoint('endpoint-test') + }) + + it('should throw if given no event', function () { + expect(remit.endpoint.bind()).to.throw('No/invalid event specified when creating an endpoint') + }) + + it('should throw if given invalid event', function () { + expect(remit.endpoint.bind(null, 123)).to.throw('No/invalid event specified when creating an endpoint') + }) + + it('should throw if given invalid options object', function () { + expect(remit.endpoint.bind(null, {})).to.throw('No/invalid event specified when creating an endpoint') + }) + + it('should return an Endpoint', function () { + expect(endpoint).to.be.an.instanceof(remit.endpoint.Type) + }) + + it('should expose a "handler" function', function () { + expect(endpoint.handler).to.be.a('function') + }) + + it('should expose an "on" function', function () { + expect(endpoint.on).to.be.a('function') + }) + + it('should expose an "options" function', function () { + expect(endpoint.options).to.be.a('function') + }) + + it('should expose a "start" function', function () { + expect(endpoint.start).to.be.a('function') + }) + }) + + describe('#usage', function () { + let remit + + before(function () { + remit = Remit({name: 'endpointRemit'}) + }) + + it('should assign new options over old ones', function () { + const endpoint = remit.endpoint('options-test') + expect(endpoint._options).to.have.property('event', 'options-test') + expect(endpoint._options).to.have.property('queue', 'options-test') + endpoint.options({queue: 'options-queue'}) + expect(endpoint._options).to.have.property('event', 'options-test') + expect(endpoint._options).to.have.property('queue', 'options-queue') + }) + + it('should not start consuming until `start` called') + + it('should throw if handler not specified on start', function () { + const endpoint = remit.endpoint('no-handler-test') + expect(endpoint.start.bind(endpoint)).to.throw('Trying to boot endpoint with no handler') + }) + + it('should allow a handler to be set when creating', function () { + const endpoint = remit.endpoint('handler-set-start', () => {}) + expect(endpoint._handler).to.be.a('function') + }) + + it('should allow a handler being set via the "handler" function', function () { + const endpoint = remit.endpoint('handler-set-later') + expect(endpoint._handler).to.equal(undefined) + endpoint.handler(() => {}) + expect(endpoint._handler).to.be.a('function') + }) + + it('should throw if "handler" run with no handlers', function () { + const endpoint = remit.endpoint('handler-no-err') + expect(endpoint.handler.bind(endpoint)).to.throw('No handler(s) given when trying to set endpoint handler(s)') + }) + + it('should return a promise on "start" that resolves when consuming', function () { + this.slow(200) + + const endpoint = remit.endpoint('on-start', () => {}) + const ret = endpoint.start() + expect(ret).to.be.a('promise') + + return ret + }) + + it('should return synchronous data', async function () { + const endpoint = await remit + .endpoint('return-1') + .handler(() => { + return 'foobar1' + }) + .start() + + const result = await remit.request('return-1')() + expect(result).to.equal('foobar1') + }) + + it('should return data via promises', async function () { + const endpoint = await remit + .endpoint('return-2') + .handler(async () => { + return 'foobar2' + }) + .start() + + const result = await remit.request('return-2')() + expect(result).to.equal('foobar2') + }) + + it('should return data via callback', async function () { + const endpoint = await remit + .endpoint('return-3') + .handler((event, callback) => { + callback(null, 'foobar3') + }) + .start() + + const result = await remit.request('return-3')() + expect(result).to.equal('foobar3') + }) + + it('should return synchronous error', async function () { + const endpoint = await remit + .endpoint('return-err-1') + .handler(() => { + throw 'fail1' + }) + .start() + + try { + await remit.request('return-err-1')() + throw new Error('Request succeeded') + } catch (e) { + expect(e).to.equal('fail1') + } + }) + + it('should return promise rejection', async function () { + const endpoint = await remit + .endpoint('return-err-2') + .handler(async () => { + throw 'fail2' + }) + .start() + + try { + await remit.request('return-err-2')() + throw new Error('Request succeeded') + } catch (e) { + expect(e).to.equal('fail2') + } + }) + + it('should return callback error', async function () { + const endpoint = await remit + .endpoint('return-err-3') + .handler((event, callback) => { + callback('fail3') + }) + .start() + + try { + await remit.request('return-err-3')() + throw new Error('Request succeeded') + } catch (e) { + expect(e).to.equal('fail3') + } + }) + + it('should return data early from synchronous middleware', async function () { + const endpoint = await remit + .endpoint('return-4') + .handler(() => { + return 'foobar4' + }, () => { + return 'fail' + }) + .start() + + const result = await remit.request('return-4')() + expect(result).to.equal('foobar4') + }) + + it('should return data early from promises middleware', async function () { + const endpoint = await remit + .endpoint('return-5') + .handler(async () => { + return 'foobar5' + }, async () => { + return 'fail' + }) + .start() + + const result = await remit.request('return-5')() + expect(result).to.equal('foobar5') + }) + + it('should return data early from callback middleware', async function () { + const endpoint = await remit + .endpoint('return-6') + .handler((event, callback) => { + callback(null, 'foobar6') + }, (event, callback) => { + callback(null, 'fail') + }) + .start() + + const result = await remit.request('return-6')() + expect(result).to.equal('foobar6') + }) + + it('should return error from synchronous middleware', async function () { + const endpoint = await remit + .endpoint('return-err-4') + .handler(() => { + throw 'fail4' + }, () => { + throw 'fail' + }) + .start() + + try { + await remit.request('return-err-4')() + throw new Error('Request succeeded') + } catch (e) { + expect(e).to.equal('fail4') + } + }) + + it('should return error from promises middleware', async function () { + const endpoint = await remit + .endpoint('return-err-5') + .handler(async () => { + throw 'fail5' + }, async () => { + throw 'fail' + }) + .start() + + try { + await remit.request('return-err-5')() + throw new Error('Request succeeded') + } catch (e) { + expect(e).to.equal('fail5') + } + }) + + it('should return error from callback middleware', async function () { + const endpoint = await remit + .endpoint('return-err-6') + .handler((event, callback) => { + callback('fail6') + }, (event, callback) => { + callback('fail') + }) + .start() + + try { + await remit.request('return-err-6')() + throw new Error('Request succeeded') + } catch (e) { + expect(e).to.equal('fail6') + } + }) + + it('should return final data from synchronous middleware', async function () { + const endpoint = await remit + .endpoint('return-7') + .handler(() => { + return + }, () => { + return 'foobar7' + }) + .start() + + const result = await remit.request('return-7')() + expect(result).to.equal('foobar7') + }) + + it('should return final data from promises middleware', async function () { + const endpoint = await remit + .endpoint('return-8') + .handler(async () => { + return + }, async () => { + return 'foobar8' + }) + .start() + + const result = await remit.request('return-8')() + expect(result).to.equal('foobar8') + }) + + it('should return final data from callback middleware', async function () { + const endpoint = await remit + .endpoint('return-9') + .handler((event, callback) => { + callback() + }, (event, callback) => { + callback(null, 'foobar9') + }) + .start() + + const result = await remit.request('return-9')() + expect(result).to.equal('foobar9') + }) + + it('should return final error from synchronous middleware', async function () { + const endpoint = await remit + .endpoint('return-err-7') + .handler(() => { + return + }, () => { + throw 'fail7' + }) + .start() + + try { + await remit.request('return-err-7')() + throw new Error('Request succeeded') + } catch (e) { + expect(e).to.equal('fail7') + } + }) + + it('should return final error from promises middleware', async function () { + const endpoint = await remit + .endpoint('return-err-8') + .handler(async () => { + return + }, async () => { + throw 'fail8' + }) + .start() + + try { + await remit.request('return-err-8')() + throw new Error('Request succeeded') + } catch (e) { + expect(e).to.equal('fail8') + } + }) + + it('should return final error from callback middleware', async function () { + const endpoint = await remit + .endpoint('return-err-9') + .handler((event, callback) => { + callback() + }, (event, callback) => { + callback('fail9') + }) + .start() + + try { + await remit.request('return-err-9')() + throw new Error('Request succeeded') + } catch (e) { + expect(e).to.equal('fail9') + } + }) + + it('should pass the same `event` to every handler', async function () { + const endpoint = await remit + .endpoint('same-event') + .handler((event) => { + event.custom = 'blamblam' + }, (event) => { + expect(event.custom).to.equal('blamblam') + + return event.custom + }) + .start() + + const result = await remit.request('same-event')() + expect(result).to.equal('blamblam') + }) + + it('should allow changing handlers realtime', async function () { + const endpoint = await remit + .endpoint('changing-handlers') + .handler(() => 'foobar') + .start() + + const req = await remit.request('changing-handlers').ready() + let res = await req() + expect(res).to.equal('foobar') + endpoint.handler(() => 'bazqux') + res = await req() + expect(res).to.equal('bazqux') + }) + + it('should throw if consumer cancelled remotely') + }) +}) diff --git a/test/exports.test.js b/test/exports.test.js new file mode 100644 index 0000000..2a8ac4e --- /dev/null +++ b/test/exports.test.js @@ -0,0 +1,41 @@ +/* global describe, it, before, expect */ +const Remit = require('../') +const version = require('../package.json').version + +describe('Remit', function () { + let remit + + before(function () { + remit = Remit() + }) + + describe('#object', function () { + it('should export the remit function', function () { + expect(Remit).to.be.a('function') + }) + + it('should export a version', function () { + expect(remit).to.have.property('version', version) + }) + + it('should export "listen" function', function () { + expect(remit.listen).to.be.a('function') + }) + + it('should export "emit" function', function () { + expect(remit.emit).to.be.a('function') + }) + + it('should export "endpoint" function', function () { + expect(remit.endpoint).to.be.a('function') + }) + + it('should export "request" function', function () { + expect(remit.request).to.be.a('function') + }) + + it('should export "on" function', function () { + expect(remit.on).to.be.a('function') + }) + }) +}) diff --git a/test/index.js b/test/index.js deleted file mode 100644 index cd6e169..0000000 --- a/test/index.js +++ /dev/null @@ -1,162 +0,0 @@ -const execSync = require('child_process').execSync; - -var prompt = require('prompt'); - -var remit = require('../index.js')({ - name: 'mocha', - url: process.env.REMIT_URL || 'amqp://localhost' -}) - -var amqp = require('amqplib') - -var chalk = require('chalk'); - -var chai = require('chai'); -var assert = chai.assert; -var expect = chai.expect; - -chai.config.includeStack = true; - -/* - -TO-DO: Once remit supports promises (or event emitters) refactor to avoid using timeouts. - - -Before hook will: -Create endpoints: - @noop - will exist but not do much. - @sum - will exist and return a sum. - @noexist - will not exist. - -Flush pertinent exchanges and queues -*/ - -describe('Remit', function() { - before(function(after) { - prompt.start(); - - var message = chalk.red.bold( - 'Proceeding will remove your rabbitmq node from any cluster it belongs to, removes all data from the management database, such as configured users and vhosts, and deletes all persistent messages. Do you wish to proceed?' - ) - - var property = { - name: 'resetCmd', - message: message, - validator: /yes|no/, - warning: 'Must respond `yes` or `no`', - default: 'no' - }; - - prompt.get(property, function(err, result) { - if (result && result.resetCmd == 'yes') { - execSync('rabbitmqctl stop_app; rabbitmqctl reset; rabbitmqctl start_app', { - stdio: [0, 1, 2] - }) - } else { - process.exit(1); - } - - remit.res('noop', (_, done) => done()) - - after() - }); - }) - - describe('#connect', function() { - it('should connect to rabbitmq', function(done) { - remit.__connect(done) - }) - }) - - describe('#res', function() { - it('should create `sum` queue', function(done) { - amqp.connect('amqp://localhost') - .then(connection => { - return connection.createChannel() - .then((channel) => { - remit.res('sum', function(nums, done) { - return done(null, nums.reduce((a, b) => a + b)) - }) - return channel - }) - .delay(500) - .tap(channel => channel.checkQueue('sum')) - .then(() => done()) - .ensure(() => connection.close()) - }); - }) - }) - - describe('#req', function() { - it('should create `remit` exchange', function(done) { - amqp.connect('amqp://localhost') - .then(connection => { - return connection.createChannel() - .tap(channel => channel.checkExchange('remit')) - .ensure(() => connection.close()) - .then(() => done()) - }); - }) - - it('should timeout after set period of 100ms', function(done) { - remit.req('noexist', {}, function(err, result) { - try { - assert.equal(err.message, 'Timed out after no response for 100ms') - done(); - } catch (err) { - done(err); - } - }, { - timeout: 100 - }) - }) - - it('should request `sum`', function(done) { - setTimeout(function() { - remit.req('sum', [7, 3], function(err, result) { - try { - assert.equal(result, 10) - done(); - } catch (err) { - done(err); - } - }) - }, 200) - }) - }) - - describe('#listen', function() { - it('should create `greeting` queue', function(done) { - amqp.connect('amqp://localhost') - .then(connection => { - return connection.createChannel() - .then((channel) => { - remit.listen('greeting', function(_, done) { - return done(null, "Hello there!") - }) - return channel - }) - .delay(200) - .tap(channel => channel.checkQueue(`greeting:emission:${remit._service_name}:${remit._listener_count}`)) - .then(() => done()) - .ensure(() => connection.close()) - }); - }) - }) - - describe('#emit', function() { - it('should emit message', function(done) { - remit.emit('greeting') - - amqp.connect('amqp://localhost') - .then(connection => { - - return connection.createChannel() - .delay(200) - .tap(channel => channel.get(`greeting:emission:${remit._service_name}:${remit._listener_count}`)) - .ensure(() => connection.close()) - .then(() => done()) - }) - }) - }) -}) diff --git a/test/listener.test.js b/test/listener.test.js new file mode 100644 index 0000000..55776ec --- /dev/null +++ b/test/listener.test.js @@ -0,0 +1,48 @@ +/* global describe, it, before, expect */ +const Remit = require('../') + +describe('Listener', function () { + let remit + + before(function () { + remit = Remit() + }) + + describe('#object', function () { + it('should be a function', function () { + expect(remit.listen).to.be.a('function') + }) + + it('should expose "on" global function', function () { + expect(remit.listen.on).to.be.a('function') + }) + }) + + describe('#return', function () { + let listener + + before(async function () { + listener = remit.listen('foo') + }) + + it('should return a Listener', function () { + expect(listener).to.be.an.instanceof(remit.listen.Type) + }) + + it('should expose a "handler" function', function () { + expect(listener.handler).to.be.a('function') + }) + + it('should expose an "on" function', function () { + expect(listener.on).to.be.a('function') + }) + + it('should expose an "options" function', function () { + expect(listener.options).to.be.a('function') + }) + + it('should expose a "start" function', function () { + expect(listener.start).to.be.a('function') + }) + }) +}) diff --git a/test/request.test.js b/test/request.test.js new file mode 100644 index 0000000..b815f2b --- /dev/null +++ b/test/request.test.js @@ -0,0 +1,22 @@ +/* global describe, it, before, expect */ +const Remit = require('../') + +describe('Request', function () { + let remit + + before(function () { + remit = Remit() + }) + + describe('#object', function () { + it('should be a function') + it('should expose "on" global function') + it('should return a Request') + it('should be runnable (#send)') + it('should expose an "on" function') + it('should expose a "fallback" function') + it('should expose an "options" function') + it('should expose a "ready" function') + it('should expose a "send" function') + }) +}) diff --git a/test/utils/CallableWrapper.test.js b/test/utils/CallableWrapper.test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/utils/asyncWaterfall.test.js b/test/utils/asyncWaterfall.test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/utils/genUuid.test.js b/test/utils/genUuid.test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/utils/generateConnectionOptions.test.js b/test/utils/generateConnectionOptions.test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/utils/getStackLine.test.js b/test/utils/getStackLine.test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/utils/handlerWrapper.test.js b/test/utils/handlerWrapper.test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/utils/parseAmqpUrl.test.js b/test/utils/parseAmqpUrl.test.js new file mode 100644 index 0000000..b03d84a --- /dev/null +++ b/test/utils/parseAmqpUrl.test.js @@ -0,0 +1,35 @@ +/* global describe, it, before */ +const Remit = require('../../') +const parse = require('../../utils/parseAmqpUrl') + +describe('parseAmqpUrl', function () { + it('should add protocol if missing', function () { + expect( + parse('localhost') + ).to.equal('amqp://localhost?frameMax=0x1000&heartbeat=15') + }) + + it('should throw if invalid protocol given', function () { + expect( + parse.bind(null, 'amq://localhost') + ).to.throw('Incorrect protocol') + }) + + it('should overwrite and merge query strings', function () { + expect( + parse('localhost?here=we&go=whoosh&heartbeat=30') + ).to.equal('amqp://localhost?frameMax=0x1000&heartbeat=30&here=we&go=whoosh') + }) + + it('should match with username and password', function () { + expect( + parse('amqp://user:pass@localhost:5672') + ).to.equal('amqp://user:pass@localhost:5672?frameMax=0x1000&heartbeat=15') + }) + + it('should match with username and password with options', function () { + expect( + parse('amqp://user:pass@localhost:5672?heartbeat=30') + ).to.equal('amqp://user:pass@localhost:5672?frameMax=0x1000&heartbeat=30') + }) +}) diff --git a/test/utils/parseEvent.test.js b/test/utils/parseEvent.test.js new file mode 100644 index 0000000..e69de29 diff --git a/test/utils/serializeData.test.js b/test/utils/serializeData.test.js new file mode 100644 index 0000000..e69de29 diff --git a/utils/CallableWrapper.js b/utils/CallableWrapper.js new file mode 100644 index 0000000..847b25a --- /dev/null +++ b/utils/CallableWrapper.js @@ -0,0 +1,38 @@ +const CallableInstance = require('callable-instance') +const EventEmitter = require('eventemitter3') + +const listeners = [ + 'data', + 'sent', + 'timeout', + 'error', + 'success' +] + +class CallableWrapper extends CallableInstance { + constructor (remit, Type) { + super('_create') + + this.remit = remit + this.Type = Type + this._emitter = new EventEmitter() + } + + on (...args) { + this._emitter.on(...args) + + return this + } + + _create (...args) { + const ret = new this.Type(this.remit, ...args) + + for (const k of listeners) { + ret.on(k, (...x) => this._emitter.emit(k, ...x)) + } + + return ret + } +} + +module.exports = CallableWrapper diff --git a/utils/ChannelPool.js b/utils/ChannelPool.js new file mode 100644 index 0000000..08fd85b --- /dev/null +++ b/utils/ChannelPool.js @@ -0,0 +1,21 @@ +const genericPool = require('generic-pool') + +function ChannelPool (connection) { + return genericPool.createPool({ + create: async () => { + const con = await connection + const channel = await con.createChannel() + channel.on('error', () => {}) + channel.on('close', () => console.log('Worker channel closed')) + + return channel + }, + + destroy: channel => channel.close() + }, { + min: 5, + max: 10 + }) +} + +module.exports = ChannelPool diff --git a/utils/asyncWaterfall.js b/utils/asyncWaterfall.js new file mode 100644 index 0000000..526aa96 --- /dev/null +++ b/utils/asyncWaterfall.js @@ -0,0 +1,33 @@ +// TODO how should middleware work? +// do we _always_ just pass back the event, making +// users mutate that to pass data along? +// that would be similar to req/res in express, +// which is good. +// or should we allow manually passing data by using +// list.shift()? +const serializeError = require('serialize-error') + +function waterfall (...fns) { + return async function (event) { + let result + + try { + for (const fn of fns) { + result = await fn(event) + + if (result !== undefined) { + return [null, result] + } + } + + return [null, result] + } catch (e) { + console.error(e) + const err = (e instanceof Error) ? serializeError(e) : e + + return [err, null] + } + } +} + +module.exports = waterfall diff --git a/utils/genUuid.js b/utils/genUuid.js new file mode 100644 index 0000000..8043d80 --- /dev/null +++ b/utils/genUuid.js @@ -0,0 +1,7 @@ +const ulid = require('ulid') + +function genUuid () { + return ulid() +} + +module.exports = genUuid diff --git a/utils/generateConnectionOptions.js b/utils/generateConnectionOptions.js new file mode 100644 index 0000000..0a2df36 --- /dev/null +++ b/utils/generateConnectionOptions.js @@ -0,0 +1,14 @@ +const packageJson = require('../package.json') + +function generateConnectionOptions (name) { + return { + clientProperties: { + connection_name: name, + powered_by: `${packageJson.name}@${packageJson.version} (${packageJson.repository.url.substr(0, packageJson.repository.url.length - 4)}/tree/${packageJson.version})`, + repository: packageJson.repository.url, + package: `https://www.npmjs.com/package/${packageJson.name}` + } + } +} + +module.exports = generateConnectionOptions diff --git a/utils/getStackLine.js b/utils/getStackLine.js new file mode 100644 index 0000000..e318e04 --- /dev/null +++ b/utils/getStackLine.js @@ -0,0 +1,25 @@ +const stack = require('callsite') + +module.exports = { + capture: function capture () { + return stack().slice(2, 4) + }, + + parse: function parse (callsites) { + let callsite + + if (callsites[1]) { + const filename = callsites[0].getFileName() + + if (filename && filename.substr(-39) === 'node_modules/callable-instance/index.js') { + callsite = callsites[1] + } else { + callsite = callsites[0] + } + } else { + callsite = callsites[0] + } + + return `${callsite.getFunctionName() || 'Object.'} (${callsite.getFileName()}:${callsite.getLineNumber()}:${callsite.getColumnNumber()})` + } +} diff --git a/utils/handlerWrapper.js b/utils/handlerWrapper.js new file mode 100644 index 0000000..33cef55 --- /dev/null +++ b/utils/handlerWrapper.js @@ -0,0 +1,39 @@ +function handlerWrapper (fn) { + return (event) => { + return new Promise((resolve, reject) => { + try { + const r = fn(event, (err, data) => { + if (err) { + reject(err) + } else { + resolve(data) + } + }) + + // if they've mapped a callback, _always_ wait for + // the callback. + // this helps clear up issues where someone has + // created an `async` function to use Promises + // but still mapped the callback to use later. + // JS is full of mixing these types, so we should + // be nice and clear on how they're handled + if (fn.length < 2) { + if (r && r.then && typeof r.then === 'function') { + // is a promise + r.then(resolve).catch(reject) + } else { + // is synchronous + resolve(r) + } + } + + // if we're here, it's using a callback, so we'll + // wait + } catch (e) { + reject(e) + } + }) + } +} + +module.exports = handlerWrapper diff --git a/utils/parseAmqpUrl.js b/utils/parseAmqpUrl.js new file mode 100644 index 0000000..cb37680 --- /dev/null +++ b/utils/parseAmqpUrl.js @@ -0,0 +1,47 @@ +const url = require('url') + +function parseUrl (input) { + const parsedUrl = url.parse(input, true) + + if (parsedUrl.protocol) { + if (parsedUrl.protocol !== 'amqp:') { + throw new Error('Incorrect protocol') + } + } else { + if (parsedUrl.pathname && !parsedUrl.host) { + parsedUrl.host = parsedUrl.pathname + parsedUrl.path = null + parsedUrl.pathname = null + parsedUrl.href = null + } + + parsedUrl.protocol = 'amqp:' + parsedUrl.slashes = true + } + + // Overwrite query parameters, as we don't want to allow + // any outside specification. + parsedUrl.query = Object.assign({}, { + // Maximum permissible size of a frame (in bytes) + // to negotiate with clients. Setting to 0 means + // "unlimited" but will trigger a bug in some QPid + // clients. Setting a larger value may improve + // throughput; setting a smaller value may improve + // latency. + // I default it to 0x1000, i.e. 4kb, which is the + // allowed minimum, will fit many purposes, and not + // chug through Node.JS's buffer pooling. + // + // frameMax: '0x20000', // 131,072 (128kb) + // + frameMax: '0x1000', // 4,096 (4kb) + heartbeat: '15' // Frequent hearbeat + }, parsedUrl.query) + + // `search` overwrites `query` if defined + parsedUrl.search = '' + + return url.format(parsedUrl) +} + +module.exports = parseUrl diff --git a/utils/parseEvent.js b/utils/parseEvent.js new file mode 100644 index 0000000..0f5042b --- /dev/null +++ b/utils/parseEvent.js @@ -0,0 +1,44 @@ +function parseEvent (properties = {}, fields = {}, data, isCustom) { + const event = { + eventId: properties.messageId, + eventType: fields.routingKey, + resource: properties.appId, + data: data + } + + if (!isCustom) { + event.started = new Date() + } + + if (properties.headers) { + if (properties.headers.uuid) { + event.eventId = properties.headers.uuid + } + + if (properties.headers.scheduled) { + event.scheduled = new Date(properties.headers.scheduled) + } + + if (properties.headers.delay) { + event.delay = properties.headers.delay + } + + if (properties.headers.trace) { + event.resourceTrace = properties.headers.trace + } + } + + if (properties.timestamp) { + let timestamp = properties.timestamp + + if (timestamp.toString().length === 10) { + timestamp *= 1000 + } + + event.timestamp = new Date(timestamp) + } + + return event +} + +module.exports = parseEvent diff --git a/utils/serializeData.js b/utils/serializeData.js new file mode 100644 index 0000000..9baab42 --- /dev/null +++ b/utils/serializeData.js @@ -0,0 +1,5 @@ +function serializeData (data) { + return JSON.stringify(data.slice(0, 2)) +} + +module.exports = serializeData