├── .coveralls.yml ├── .eslintrc ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── npmpublish.yml │ └── pushtest.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── 404.html ├── Gemfile ├── Gemfile.lock ├── _api │ ├── emit.md │ ├── endpoint.md │ ├── listen.md │ ├── remit.md │ └── request.md ├── _change │ └── v2.3.3.md ├── _config.yml ├── _guide │ ├── concepts.md │ ├── consumer-groups.md │ ├── events.md │ ├── handlers.md │ ├── internals.md │ ├── middleware.md │ └── tracing.md ├── _includes │ ├── body.html │ ├── content.html │ ├── critical.scss │ ├── head.html │ ├── header.html │ ├── sidebar.html │ ├── sticky-sidebar.html │ └── sw.html ├── _layouts │ └── default.html ├── _posts │ └── 2018-03-15-welcome-to-jekyll.markdown ├── _start │ ├── installing.md │ └── simple-example.md ├── about.md ├── css │ ├── monokai.css │ └── syntax.scss ├── index.md ├── js │ └── sticky-sidebar.min.js ├── manifest.json └── sw.js ├── index.d.ts ├── index.js ├── lib ├── Emitter.js ├── Endpoint.js ├── Listener.js ├── Remit.js └── Request.js ├── package-lock.json ├── package.json ├── test ├── bootstrap.js ├── connection.test.js ├── emitter.test.js ├── endpoint.test.js ├── exports.test.js ├── listener.test.js ├── request.test.js └── utils │ ├── CallableWrapper.test.js │ ├── asyncWaterfall.test.js │ ├── genUuid.test.js │ ├── generateConnectionOptions.test.js │ ├── getStackLine.test.js │ ├── handlerWrapper.test.js │ ├── parseAmqpUrl.test.js │ ├── parseEvent.test.js │ └── serializeData.test.js └── utils ├── CallableWrapper.js ├── ChannelPool.js ├── asyncWaterfall.js ├── genUuid.js ├── generateConnectionOptions.js ├── getStackLine.js ├── handlerWrapper.js ├── parseAmqpUrl.js ├── parseEvent.js ├── serializeData.js └── throwAsException.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: 0rxp9epmxLHCaWKPvbWyizfsPFdp68Cs5 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 8, 4 | "ecmaFeatures": { 5 | "experimentalObjectRestSpread": true, 6 | "jsx": true 7 | }, 8 | "sourceType": "module" 9 | }, 10 | 11 | "env": { 12 | "es6": true, 13 | "node": true 14 | }, 15 | 16 | "plugins": [ 17 | "import", 18 | "node", 19 | "promise", 20 | "standard" 21 | ], 22 | 23 | "globals": { 24 | "document": false, 25 | "navigator": false, 26 | "window": false 27 | }, 28 | 29 | "rules": { 30 | "accessor-pairs": "error", 31 | "arrow-spacing": ["error", { "before": true, "after": true }], 32 | "block-spacing": ["error", "always"], 33 | "brace-style": ["error", "1tbs", { "allowSingleLine": true }], 34 | "camelcase": ["error", { "properties": "never" }], 35 | "comma-dangle": ["error", { 36 | "arrays": "never", 37 | "objects": "never", 38 | "imports": "never", 39 | "exports": "never", 40 | "functions": "never" 41 | }], 42 | "comma-spacing": ["error", { "before": false, "after": true }], 43 | "comma-style": ["error", "last"], 44 | "constructor-super": "error", 45 | "curly": ["error", "multi-line"], 46 | "dot-location": ["error", "property"], 47 | "eol-last": "error", 48 | "eqeqeq": ["error", "always", { "null": "ignore" }], 49 | "func-call-spacing": ["error", "never"], 50 | "generator-star-spacing": ["error", { "before": true, "after": true }], 51 | "handle-callback-err": ["error", "^(err|error)$" ], 52 | "indent": ["error", 2, { "SwitchCase": 1 }], 53 | "key-spacing": ["error", { "beforeColon": false, "afterColon": true }], 54 | "keyword-spacing": ["error", { "before": true, "after": true }], 55 | "new-cap": ["error", { "newIsCap": true, "capIsNew": false }], 56 | "new-parens": "error", 57 | "no-array-constructor": "error", 58 | "no-caller": "error", 59 | "no-class-assign": "error", 60 | "no-compare-neg-zero": "error", 61 | "no-cond-assign": "error", 62 | "no-const-assign": "error", 63 | "no-constant-condition": ["error", { "checkLoops": false }], 64 | "no-control-regex": "error", 65 | "no-debugger": "error", 66 | "no-delete-var": "error", 67 | "no-dupe-args": "error", 68 | "no-dupe-class-members": "error", 69 | "no-dupe-keys": "error", 70 | "no-duplicate-case": "error", 71 | "no-empty-character-class": "error", 72 | "no-empty-pattern": "error", 73 | "no-eval": "error", 74 | "no-ex-assign": "error", 75 | "no-extend-native": "error", 76 | "no-extra-bind": "error", 77 | "no-extra-boolean-cast": "error", 78 | "no-extra-parens": ["error", "functions"], 79 | "no-fallthrough": "error", 80 | "no-floating-decimal": "error", 81 | "no-func-assign": "error", 82 | "no-global-assign": "error", 83 | "no-implied-eval": "error", 84 | "no-inner-declarations": ["error", "functions"], 85 | "no-invalid-regexp": "error", 86 | "no-irregular-whitespace": "error", 87 | "no-iterator": "error", 88 | "no-label-var": "error", 89 | "no-labels": ["error", { "allowLoop": false, "allowSwitch": false }], 90 | "no-lone-blocks": "error", 91 | "no-mixed-operators": ["error", { 92 | "groups": [ 93 | ["==", "!=", "===", "!==", ">", ">=", "<", "<="], 94 | ["&&", "||"], 95 | ["in", "instanceof"] 96 | ], 97 | "allowSamePrecedence": true 98 | }], 99 | "no-mixed-spaces-and-tabs": "error", 100 | "no-multi-spaces": "error", 101 | "no-multi-str": "error", 102 | "no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }], 103 | "no-negated-in-lhs": "error", 104 | "no-new": "error", 105 | "no-new-func": "error", 106 | "no-new-object": "error", 107 | "no-new-require": "error", 108 | "no-new-symbol": "error", 109 | "no-new-wrappers": "error", 110 | "no-obj-calls": "error", 111 | "no-octal": "error", 112 | "no-octal-escape": "error", 113 | "no-path-concat": "error", 114 | "no-proto": "error", 115 | "no-redeclare": "error", 116 | "no-regex-spaces": "error", 117 | "no-return-assign": ["error", "except-parens"], 118 | "no-return-await": "error", 119 | "no-self-assign": "error", 120 | "no-self-compare": "error", 121 | "no-sequences": "error", 122 | "no-shadow-restricted-names": "error", 123 | "no-sparse-arrays": "error", 124 | "no-tabs": "error", 125 | "no-template-curly-in-string": "error", 126 | "no-this-before-super": "error", 127 | "no-throw-literal": "error", 128 | "no-trailing-spaces": "error", 129 | "no-undef": "error", 130 | "no-undef-init": "error", 131 | "no-unexpected-multiline": "error", 132 | "no-unmodified-loop-condition": "error", 133 | "no-unneeded-ternary": ["error", { "defaultAssignment": false }], 134 | "no-unreachable": "error", 135 | "no-unsafe-finally": "error", 136 | "no-unsafe-negation": "error", 137 | "no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true, "allowTaggedTemplates": true }], 138 | "no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": true }], 139 | "no-use-before-define": ["error", { "functions": false, "classes": false, "variables": false }], 140 | "no-useless-call": "error", 141 | "no-useless-computed-key": "error", 142 | "no-useless-constructor": "error", 143 | "no-useless-escape": "error", 144 | "no-useless-rename": "error", 145 | "no-useless-return": "error", 146 | "no-whitespace-before-property": "error", 147 | "no-with": "error", 148 | "object-property-newline": ["error", { "allowMultiplePropertiesPerLine": true }], 149 | "one-var": ["error", { "initialized": "never" }], 150 | "operator-linebreak": ["error", "after", { "overrides": { "?": "before", ":": "before" } }], 151 | "padded-blocks": ["error", { "blocks": "never", "switches": "never", "classes": "never" }], 152 | "prefer-promise-reject-errors": "error", 153 | "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], 154 | "rest-spread-spacing": ["error", "never"], 155 | "semi": ["error", "never"], 156 | "semi-spacing": ["error", { "before": false, "after": true }], 157 | "space-before-blocks": ["error", "always"], 158 | "space-before-function-paren": ["error", "always"], 159 | "space-in-parens": ["error", "never"], 160 | "space-infix-ops": "error", 161 | "space-unary-ops": ["error", { "words": true, "nonwords": false }], 162 | "spaced-comment": ["error", "always", { 163 | "line": { "markers": ["*package", "!", "/", ","] }, 164 | "block": { "balanced": true, "markers": ["*package", "!", ",", ":", "::", "flow-include"], "exceptions": ["*"] } 165 | }], 166 | "symbol-description": "error", 167 | "template-curly-spacing": ["error", "never"], 168 | "template-tag-spacing": ["error", "never"], 169 | "unicode-bom": ["error", "never"], 170 | "use-isnan": "error", 171 | "valid-typeof": ["error", { "requireStringLiterals": true }], 172 | "wrap-iife": ["error", "any", { "functionPrototypeMethods": true }], 173 | "yield-star-spacing": ["error", "both"], 174 | "yoda": ["error", "never"], 175 | 176 | "import/export": "error", 177 | "import/first": "error", 178 | "import/no-duplicates": "error", 179 | "import/no-webpack-loader-syntax": "error", 180 | 181 | "node/no-deprecated-api": "error", 182 | "node/process-exit-as-throw": "error", 183 | 184 | "promise/param-names": "error", 185 | 186 | "standard/array-bracket-even-spacing": ["error", "either"], 187 | "standard/computed-property-even-spacing": ["error", "even"], 188 | "standard/no-callback-literal": "error", 189 | "standard/object-curly-even-spacing": ["error", "either"] 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jpwilliams 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 99 8 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | services: 13 | rabbitmq: 14 | image: rabbitmq:latest 15 | ports: 16 | - 5672/tcp 17 | options: --health-cmd "rabbitmqctl node_health_check" --health-interval 10s --health-timeout 5s --health-retries 5 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - uses: actions/setup-node@v1 22 | with: 23 | node-version: 12 24 | - run: npm ci 25 | - name: 'test package' 26 | run: npm test 27 | env: 28 | REMIT_URL: amqp://localhost:${{ job.services.rabbitmq.ports[5672] }} 29 | 30 | publish-npm: 31 | needs: build 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v1 35 | - uses: actions/setup-node@v1 36 | with: 37 | node-version: 12 38 | registry-url: https://registry.npmjs.org/ 39 | - run: npm publish --access public 40 | env: 41 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 42 | 43 | publish-gpr: 44 | needs: build 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v1 48 | - uses: actions/setup-node@v1 49 | with: 50 | node-version: 12 51 | registry-url: https://npm.pkg.github.com/ 52 | scope: '@jpwilliams' 53 | - name: 'publish to gpr' 54 | run: | 55 | npm publish --registry=https://npm.pkg.github.com/jpwilliams --scope=@jpwilliams 56 | env: 57 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 58 | -------------------------------------------------------------------------------- /.github/workflows/pushtest.yml: -------------------------------------------------------------------------------- 1 | name: Test on push 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | services: 10 | rabbitmq: 11 | image: rabbitmq:latest 12 | ports: 13 | - 5672/tcp 14 | options: --health-cmd "rabbitmqctl node_health_check" --health-interval 10s --health-timeout 5s --health-retries 5 15 | 16 | steps: 17 | - uses: actions/checkout@v1 18 | - uses: actions/setup-node@v1 19 | with: 20 | node-version: 12 21 | - run: npm ci 22 | - name: 'test package' 23 | run: npm test 24 | env: 25 | REMIT_URL: amqp://localhost:${{ job.services.rabbitmq.ports[5672] }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directories used by tools like istanbul 15 | coverage 16 | .nyc_output 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 29 | node_modules 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: node_js 3 | addons: 4 | apt: 5 | packages: 6 | - rabbitmq-server 7 | services: 8 | - rabbitmq 9 | sudo: required 10 | node_js: 11 | - "node" 12 | cache: yarn 13 | script: "npm run travis" 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jack Williams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @jpwilliams/remit 2 | 3 | [![Build Status](https://travis-ci.org/jpwilliams/remit.svg?branch=master)](https://travis-ci.org/jpwilliams/remit) [![Coverage Status](https://coveralls.io/repos/github/jpwilliams/remit/badge.svg?branch=master)](https://coveralls.io/github/jpwilliams/remit?branch=master) [![npm downloads per month](https://img.shields.io/npm/dm/@jpwilliams/remit.svg)](https://www.npmjs.com/package/@jpwilliams/remit) [![npm version](https://img.shields.io/npm/v/@jpwilliams/remit.svg)](https://www.npmjs.com/package/@jpwilliams/remit) [![OpenTracing Badge](https://img.shields.io/badge/OpenTracing-enabled-blue.svg)](http://opentracing.io) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fjpwilliams%2Fremit.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fjpwilliams%2Fremit?ref=badge_shield) 4 | 5 | A wrapper for RabbitMQ for communication between microservices. No service discovery needed. 6 | 7 | ``` sh 8 | npm install @jpwilliams/remit 9 | ``` 10 | 11 | ``` js 12 | const Remit = require('@jpwilliams/remit') 13 | const remit = Remit({ name: 'user-service' }) 14 | 15 | remit 16 | .endpoint('user') 17 | .handler((event) => { 18 | return { 19 | name: 'Jack Williams', 20 | email: 'jack@wildfire.gg' 21 | } 22 | }) 23 | .start() 24 | 25 | // another service/process 26 | const Remit = require('@jpwilliams/remit') 27 | const remit = Remit({ name: 'api' }) 28 | 29 | const getUser = remit.request('user') 30 | const user = await getUser(123) 31 | console.log(user) 32 | 33 | /* { 34 | name: 'Jack Williams', 35 | email: 'jack@wildfire.gg' 36 | } */ 37 | ``` 38 | 39 | --- 40 | 41 | ## What's remit? 42 | 43 | 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. 44 | 45 | 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** across grouped services and redundant; if a service dies, another will pick up the slack. 46 | 47 | There are four types you can use with Remit. 48 | 49 | * [request](#), which fetches data from an [endpoint](#) 50 | * [emit](#), which emits data to [listen](#)ers 51 | 52 | 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: when creating multiple instances of a service, make sure they all have the same name. 53 | 54 | --- 55 | 56 | ## Contents 57 | 58 | * [What's remit?](#) 59 | * [Recommendations](#) 60 | * [API/Usage](#) 61 | * [Events](#) 62 | * [Handlers](#) 63 | * [Tracing](#tracing) 64 | 65 | --- 66 | 67 | ## API/Usage 68 | 69 | * [request(event)](#) 70 | * [request.on(eventName, listener)](#) 71 | * [request.fallback(data)](#) 72 | * [request.options(options)](#) 73 | * [request.ready()](#) 74 | * [request.send([data[, options]]) OR request([data[, options]])](#) 75 | * [endpoint(event[, ...handlers])](#) 76 | * [endpoint.handler(...handlers)](#) 77 | * [endpoint.on(eventName, listener)](#) 78 | * [endpoint.options(options)](#) 79 | * [endpoint.start()](#) 80 | * [endpoint.pause()](#) 81 | * [endpoint.resume()](#) 82 | * [emit(event)](#) 83 | * [emit.on(eventName, listener)](#) 84 | * [emit.options(options)](#) 85 | * [emit.ready()](#) 86 | * [emit.send([data[, options]]) OR emit([data[, options]])](#) 87 | * [listen(event[, ...handlers])](#) 88 | * [listen.handler(...handlers)](#) 89 | * [listen.on(eventName, listener)](#) 90 | * [listen.options(options)](#) 91 | * [listen.start()](#) 92 | * [listen.pause()](#) 93 | * [listen.resume()](#) 94 | 95 | --- 96 | 97 | #### `request(event)` 98 | 99 | * `event` <string> | <Object> 100 | 101 | 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. 102 | 103 | ``` js 104 | remit.request('foo.bar') 105 | ``` 106 | 107 | `timeout` and `priority` are explained and can be changed at any stage using [`request.options()`](#). 108 | 109 | The request is sent by running the returned function (synonymous with calling `.send()`), passing the data you wish the make the request with. 110 | 111 | For example, to retrieve a user from the `'user.profile'` endpoint using an ID: 112 | 113 | ``` js 114 | const getUserProfile = remit.request('user.profile') 115 | const user = await getUserProfile(123) 116 | console.log(user) 117 | // prints the user's data 118 | ``` 119 | 120 | Returns a new request. 121 | 122 | #### `request.on(eventName, listener)` 123 | 124 | * `eventName` <any> 125 | * `listener` <Function> 126 | 127 | Subscribe to this request's dumb EventEmitter. For more information on the events emitted, see the [Events](#) section. 128 | 129 | Returns a reference to the `request`, so that calls can be chained. 130 | 131 | #### `request.fallback(data)` 132 | 133 | * `data` <any> 134 | 135 | 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. 136 | 137 | ``` js 138 | const request = remit 139 | .request('user.list') 140 | .fallback([]) 141 | ``` 142 | 143 | The error is still sent over the request's EventEmitter, so listening to `'error'` lets you handle the error however you wish. 144 | 145 | You can change the fallback at any point in a request's life and unset it by passing no arguments to the function. 146 | 147 | Returns a reference to the `request`, so that calls can be chained. 148 | 149 | #### `request.options(options)` 150 | 151 | * `options` <Object> 152 | * `event` <string> **Required** 153 | * `timeout` <integer> **Default:** `30000` 154 | * `priority` <integer> **Default:** `0` 155 | 156 | 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. 157 | 158 | ``` js 159 | const request = remit 160 | .request('foo.bar') 161 | .options({ 162 | timeout: 5000 163 | }) 164 | ``` 165 | 166 | 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". 167 | 168 | `priority` can be an integer between `0` and `10`. Higher priority requests will go to the front of queues over lower priority requests. 169 | 170 | Returns a reference to the `request`, so that calls can be chained. 171 | 172 | #### `request.ready()` 173 | 174 | Returns a promise which resolves when the request is ready to make calls. 175 | 176 | ``` js 177 | const request = await remit 178 | .request('foo.bar') 179 | .ready() 180 | ``` 181 | 182 | Any calls made before this promise is resolved will be automatically queued until it is. 183 | 184 | Returns a reference to the `request`, so that calls can be chained. 185 | 186 | #### `request.send([data[, options]])` 187 | 188 | _Synonymous with `request([data[, options]])`_ 189 | 190 | * `data` <any> **Default:** `null` 191 | * `options` <Object> 192 | 193 | 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). 194 | 195 | ``` js 196 | const getUser = remit.request('user.getProfile') 197 | 198 | // either of these perform the same action 199 | const user = await getUser(123) 200 | const user = await getUser.send(123) 201 | ``` 202 | 203 | `options` can contain anything provided in [`request.options`](#), but the options provided will only apply to that single request. 204 | 205 | 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. 206 | 207 | --- 208 | 209 | #### `endpoint(event[, ...handlers])` 210 | 211 | * `event` <string> | <Object> 212 | * `...handlers` <Function> 213 | 214 | Creates an endpoint that replies to [`request`](#)s. 215 | 216 | `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`](#). 217 | 218 | ``` js 219 | const endpoint = await remit 220 | .endpoint('foo.bar', console.log) 221 | .start() 222 | ``` 223 | 224 | [`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. 225 | 226 | Returns a new endpoint. 227 | 228 | #### `endpoint.handler(...handlers)` 229 | 230 | * `...handlers` <Function> 231 | 232 | 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. 233 | 234 | ``` js 235 | const endpoint = remit.endpoint('foo.bar') 236 | endpoint.handler(logRequest, sendFoo) 237 | endpoint.start() 238 | ``` 239 | 240 | For more information on handlers, see the [Handlers](#) section. 241 | 242 | Returns a reference to the `endpoint`, so that calls can be chained. 243 | 244 | #### `endpoint.on(eventName, listener)` 245 | 246 | * `eventName` <any> 247 | * `listener` <Function> 248 | 249 | Subscribe to this endpoint's dumb EventEmitter. For more information on the events emitted, see the [Events](#) section. 250 | 251 | Returns a reference to the `endpoint`, so that calls can be chained. 252 | 253 | #### `endpoint.options(options)` 254 | 255 | #### `endpoint.start()` 256 | 257 | #### `endpoint.pause([cold])` 258 | 259 | * `cold` <Boolean> 260 | 261 | Pauses consumption of messages for this endpoint. By default, any messages currently in memory will be processed (a "warm" pause). If `cold` is provided as truthy, any messages in memory will be pushed back to RabbitMQ. 262 | 263 | Has no effect if the endpoint is already paused or has not yet been started. 264 | 265 | Returns a promise that resolves with the endpoint when consumption has been successfully paused. 266 | 267 | #### `endpoint.resume()` 268 | 269 | Resumes consumption of messages for this endpoint after being paused using [`pause()`](#). If run on an endpoint that is not yet started, the endpoint will attempt to start. 270 | 271 | Returns a promise that resolves with the endpoint when consumption has been successfully resumed. 272 | 273 | ---- 274 | 275 | #### `emit.on(eventName, listener)` 276 | 277 | * `eventName` <any> 278 | * `listener` <Function> 279 | 280 | Subscribe to this emitter's dumb EventEmitter. For more information on the events emitted, see the [Events](#) section. 281 | 282 | Returns a reference to the `emit`, so that calls can be chained. 283 | 284 | #### `emit.options(options)` 285 | 286 | #### `emit.ready()` 287 | 288 | #### `emit.send([data[, options]])` 289 | 290 | --- 291 | 292 | #### `listen.handler(...handlers)` 293 | 294 | #### `listen.on(eventName, listener)` 295 | 296 | * `eventName` <any> 297 | * `listener` <Function> 298 | 299 | Subscribe to this listener's dumb EventEmitter. For more information on the events emitted, see the [Events](#) section. 300 | 301 | Returns a reference to the `listen`, so that calls can be chained. 302 | 303 | #### `listen.options(options)` 304 | 305 | #### `listen.start()` 306 | 307 | #### `listen.pause([cold])` 308 | 309 | * `cold` <Boolean> 310 | 311 | Pauses consumption of messages for this listener. By default, any messages currently in memory will be processed (a "warm" pause). If `cold` is provided as truthy, any messages in memory will be pushed back to RabbitMQ. 312 | 313 | Has no effect if the listener is already paused or has not yet been started. 314 | 315 | Returns a promise that resolves with the listener when consumption has been successfully paused. 316 | 317 | #### `listen.resume()` 318 | 319 | Resumes consumption of messages for this listener after being paused using [`pause()`](#). If run on a listener that is not yet started, the listener will attempt to start. 320 | 321 | Returns a promise that resolves with the listener when consumption has been successfully resumed. 322 | 323 | --- 324 | 325 | ## Events 326 | 327 | [`request`](#), [`endpoint`](#), [`emit`](#) and [`listen`](#) all export EventEmitters that emit events about their incoming/outgoing messages. 328 | 329 | All of the events can be listened to by using the `.on()` function, providing an `eventName` and a `listener` function, like so: 330 | 331 | ``` js 332 | const request = remit.request('foo.bar') 333 | const endpoint = remit.endpoint('foo.bar') 334 | const emit = remit.emit('foo.bar') 335 | const listen = remit.listen('foo.bar') 336 | 337 | request.on('...', ...) 338 | endpoint.on('...', ...) 339 | emit.on('...', ...) 340 | listen.on('...', ...) 341 | ``` 342 | 343 | 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. 344 | 345 | ``` js 346 | remit.request.on('...', ...) 347 | remit.endpoint.on('...', ...) 348 | remit.emit.on('...', ...) 349 | remit.listen.on('...', ...) 350 | ``` 351 | 352 | The following events can be listened to: 353 | 354 | | Event | Description | Returns | request | endpoint | emit | listen | 355 | | ----- | ----------- | ------- | :---: | :---: | :---: | :---: | 356 | | `data` | Data was received | Raw data | ✅ | ✅ | ❌ | ✅ | 357 | | `error` | An error occured or was passed back from an endpoint | Error | ✅ | ✅ | ✅ | ✅ | 358 | | `sent` | Data was sent | The event that was sent | ✅ | ✅ | ✅ | ❌ | 359 | | `success` | The action was successful | The successful result/data | ✅ | ✅ | ✅ | ✅ | 360 | | `timeout` | The request timed out | A [timeout object](#) | ✅ | ❌ | ❌ | ❌ | 361 | 362 | ## Handlers 363 | 364 | [Endpoints](#) and [listeners](#) use handlers to reply to or, uh, handle incoming messages. In both cases, these are functions or values that can be passed when creating the listener or added/changed real-time by using the `.handler()` method. 365 | 366 | If a handler is a value (i.e. not a function) then it will be returned as the data of a successful response. This is useful for simple endpoints that just need to return static or just simple mutable values. 367 | 368 | All handler _functions_ 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. 369 | 370 | 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. 371 | 372 | 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). 373 | 374 | #### Simple returns 375 | 376 | Here, we create a simple endpoint that returns `{"foo": "bar"}` whenever called: 377 | 378 | ``` js 379 | const endpoint = await remit 380 | .endpoint('foo.bar', () => { 381 | return {foo: 'bar'} 382 | }) 383 | .start() 384 | ``` 385 | 386 | #### Incoming data 387 | 388 | We can also parse incoming data and gather information on the request by using the given `event` object. 389 | 390 | ``` js 391 | const endpoint = await remit 392 | .endpoint('foo.bar', (event) => { 393 | console.log(event) 394 | }) 395 | .start() 396 | ``` 397 | 398 | #### Event object 399 | 400 | When called, the above will log out the event it's been passed. Here's an example of an event object: 401 | 402 | ``` js 403 | { 404 | started: , // time the message was taken from the server 405 | eventId: , // a unique ID for the message (useful for idempotency purposes) 406 | eventType: 'foo.bar', // the eventName used to call this endpoint/listener (useful when using wildcard listeners) 407 | resource: 'service-user', // the name of the service that called/emitted this 408 | data: {userId: 123}, // the data sent with the request 409 | timestamp: , // the time the message was created 410 | 411 | // extraneous information, currently containing tracing data 412 | metadata: { 413 | originId: , // the ID of the initial request or emission that started the entire chain of calls - every call in a chain will have the same ID here 414 | bubbleId: , // the "bubble" (see more below) that the action happened in 415 | fromBubbleId: , // the "bubble" (see more below) that the action was triggered from 416 | instanceId: , // a unique ID for every action 417 | flowType: // either `'entry'` to show it's an entrypoint to a bubble, `'exit'` to show it's an exit from a bubble or blank to show it is neither 418 | } 419 | } 420 | ``` 421 | 422 | #### Handling completion 423 | 424 | Handlers provide you with four different ways of showing completion: Promises, callbacks, a synchronous call or a straight value. To decide what the handler should treat as a successful result, remit follows the following pattern: 425 | 426 | ``` 427 | if handler is not a function: 428 | ├── Return handler value 429 | else if handler does not map second (callback) property: 430 | ├── if handler returns a promise: 431 | │ └── Watch resolution/rejection of result 432 | │ else: 433 | │ └── Return synchronous result 434 | else: 435 | └── Wait for callback to be called 436 | ``` 437 | 438 | 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). 439 | 440 | #### Middleware 441 | 442 | 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. 443 | 444 | A common use case for middleware is validation. Here, a middleware handler adds a property to incoming data before continuing: 445 | 446 | ``` js 447 | const endpoint = await remit 448 | .endpoint('foo.bar') 449 | .handler((event) => { 450 | event.foo = 'bar' 451 | }, (event) => { 452 | console.log(event) 453 | // event will contain `foo: 'bar'` 454 | 455 | return true 456 | }) 457 | .start() 458 | ``` 459 | 460 | 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. 461 | 462 | If an exception is thrown at any point, the chain will also break and the error will be returned to the requester. 463 | 464 | This means you can fall out of chains early. Let's say we want to fake an empty response for user #21: 465 | 466 | ``` js 467 | const endpoint = await remit 468 | .endpoint('foo.bar') 469 | .handler((event) => { 470 | if (event.data.userId === 21) { 471 | return [] 472 | } 473 | }, (event) => { 474 | return calculateUserList() 475 | }) 476 | .start() 477 | ``` 478 | 479 | Or perhaps exit if a call is done with no authorisation token: 480 | 481 | ``` js 482 | const endpoint = await remit 483 | .endpoint('foo.bar') 484 | .handler(async (event) => { 485 | if (!event.data.authToken) { 486 | throw new Error('No authorisation token given') 487 | } 488 | 489 | event.data.decodedToken = await decodeAuthToken(event.data.authToken) 490 | }, (event) => { 491 | return performGuardedCall() 492 | }) 493 | ``` 494 | 495 | ## Tracing 496 | 497 | See [`remitrace`](https://github.com/jpwilliams/remitrace). 498 | 499 | 500 | ## License 501 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fjpwilliams%2Fremit.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fjpwilliams%2Fremit?ref=badge_large) -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-metadata 4 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | 18 | 19 |
20 |

404

21 | 22 |

Page not found :(

23 |

The requested page could not be found.

24 |
25 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Hello! This is where you manage which Jekyll version is used to run. 4 | # When you want to use a different version, change it below, save the 5 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 6 | # 7 | # bundle exec jekyll serve 8 | # 9 | # This will help ensure the proper Jekyll version is running. 10 | # Happy Jekylling! 11 | # gem "jekyll", "~> 3.7.3" 12 | 13 | # This is the default theme for new Jekyll sites. You may change this to anything you like. 14 | # gem "minima", "~> 2.0" 15 | 16 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 17 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 18 | gem "github-pages", group: :jekyll_plugins 19 | 20 | # If you have any plugins, put them here! 21 | group :jekyll_plugins do 22 | gem "jekyll-feed", "~> 0.6" 23 | end 24 | 25 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 26 | gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] 27 | 28 | # Performance-booster for watching directories on Windows 29 | gem "wdm", "~> 0.1.0" if Gem.win_platform? 30 | 31 | -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (4.2.9) 5 | i18n (~> 0.7) 6 | minitest (~> 5.1) 7 | thread_safe (~> 0.3, >= 0.3.4) 8 | tzinfo (~> 1.1) 9 | addressable (2.5.2) 10 | public_suffix (>= 2.0.2, < 4.0) 11 | coffee-script (2.4.1) 12 | coffee-script-source 13 | execjs 14 | coffee-script-source (1.11.1) 15 | colorator (1.1.0) 16 | commonmarker (0.17.9) 17 | ruby-enum (~> 0.5) 18 | concurrent-ruby (1.0.5) 19 | ethon (0.11.0) 20 | ffi (>= 1.3.0) 21 | execjs (2.7.0) 22 | faraday (0.14.0) 23 | multipart-post (>= 1.2, < 3) 24 | ffi (1.9.23) 25 | forwardable-extended (2.6.0) 26 | gemoji (3.0.0) 27 | github-pages (179) 28 | activesupport (= 4.2.9) 29 | github-pages-health-check (= 1.4.0) 30 | jekyll (= 3.6.2) 31 | jekyll-avatar (= 0.5.0) 32 | jekyll-coffeescript (= 1.1.1) 33 | jekyll-commonmark-ghpages (= 0.1.5) 34 | jekyll-default-layout (= 0.1.4) 35 | jekyll-feed (= 0.9.3) 36 | jekyll-gist (= 1.5.0) 37 | jekyll-github-metadata (= 2.9.4) 38 | jekyll-mentions (= 1.3.0) 39 | jekyll-optional-front-matter (= 0.3.0) 40 | jekyll-paginate (= 1.1.0) 41 | jekyll-readme-index (= 0.2.0) 42 | jekyll-redirect-from (= 0.13.0) 43 | jekyll-relative-links (= 0.5.3) 44 | jekyll-remote-theme (= 0.2.3) 45 | jekyll-sass-converter (= 1.5.2) 46 | jekyll-seo-tag (= 2.4.0) 47 | jekyll-sitemap (= 1.2.0) 48 | jekyll-swiss (= 0.4.0) 49 | jekyll-theme-architect (= 0.1.0) 50 | jekyll-theme-cayman (= 0.1.0) 51 | jekyll-theme-dinky (= 0.1.0) 52 | jekyll-theme-hacker (= 0.1.0) 53 | jekyll-theme-leap-day (= 0.1.0) 54 | jekyll-theme-merlot (= 0.1.0) 55 | jekyll-theme-midnight (= 0.1.0) 56 | jekyll-theme-minimal (= 0.1.0) 57 | jekyll-theme-modernist (= 0.1.0) 58 | jekyll-theme-primer (= 0.5.2) 59 | jekyll-theme-slate (= 0.1.0) 60 | jekyll-theme-tactile (= 0.1.0) 61 | jekyll-theme-time-machine (= 0.1.0) 62 | jekyll-titles-from-headings (= 0.5.1) 63 | jemoji (= 0.9.0) 64 | kramdown (= 1.16.2) 65 | liquid (= 4.0.0) 66 | listen (= 3.1.5) 67 | mercenary (~> 0.3) 68 | minima (= 2.4.0) 69 | nokogiri (>= 1.8.1, < 2.0) 70 | rouge (= 2.2.1) 71 | terminal-table (~> 1.4) 72 | github-pages-health-check (1.4.0) 73 | addressable (~> 2.3) 74 | net-dns (~> 0.8) 75 | octokit (~> 4.0) 76 | public_suffix (~> 2.0) 77 | typhoeus (~> 1.3) 78 | html-pipeline (2.7.1) 79 | activesupport (>= 2) 80 | nokogiri (>= 1.4) 81 | i18n (0.9.5) 82 | concurrent-ruby (~> 1.0) 83 | jekyll (3.6.2) 84 | addressable (~> 2.4) 85 | colorator (~> 1.0) 86 | jekyll-sass-converter (~> 1.0) 87 | jekyll-watch (~> 1.1) 88 | kramdown (~> 1.14) 89 | liquid (~> 4.0) 90 | mercenary (~> 0.3.3) 91 | pathutil (~> 0.9) 92 | rouge (>= 1.7, < 3) 93 | safe_yaml (~> 1.0) 94 | jekyll-avatar (0.5.0) 95 | jekyll (~> 3.0) 96 | jekyll-coffeescript (1.1.1) 97 | coffee-script (~> 2.2) 98 | coffee-script-source (~> 1.11.1) 99 | jekyll-commonmark (1.1.0) 100 | commonmarker (~> 0.14) 101 | jekyll (>= 3.0, < 4.0) 102 | jekyll-commonmark-ghpages (0.1.5) 103 | commonmarker (~> 0.17.6) 104 | jekyll-commonmark (~> 1) 105 | rouge (~> 2) 106 | jekyll-default-layout (0.1.4) 107 | jekyll (~> 3.0) 108 | jekyll-feed (0.9.3) 109 | jekyll (~> 3.3) 110 | jekyll-gist (1.5.0) 111 | octokit (~> 4.2) 112 | jekyll-github-metadata (2.9.4) 113 | jekyll (~> 3.1) 114 | octokit (~> 4.0, != 4.4.0) 115 | jekyll-mentions (1.3.0) 116 | activesupport (~> 4.0) 117 | html-pipeline (~> 2.3) 118 | jekyll (~> 3.0) 119 | jekyll-optional-front-matter (0.3.0) 120 | jekyll (~> 3.0) 121 | jekyll-paginate (1.1.0) 122 | jekyll-readme-index (0.2.0) 123 | jekyll (~> 3.0) 124 | jekyll-redirect-from (0.13.0) 125 | jekyll (~> 3.3) 126 | jekyll-relative-links (0.5.3) 127 | jekyll (~> 3.3) 128 | jekyll-remote-theme (0.2.3) 129 | jekyll (~> 3.5) 130 | rubyzip (>= 1.2.1, < 3.0) 131 | typhoeus (>= 0.7, < 2.0) 132 | jekyll-sass-converter (1.5.2) 133 | sass (~> 3.4) 134 | jekyll-seo-tag (2.4.0) 135 | jekyll (~> 3.3) 136 | jekyll-sitemap (1.2.0) 137 | jekyll (~> 3.3) 138 | jekyll-swiss (0.4.0) 139 | jekyll-theme-architect (0.1.0) 140 | jekyll (~> 3.5) 141 | jekyll-seo-tag (~> 2.0) 142 | jekyll-theme-cayman (0.1.0) 143 | jekyll (~> 3.5) 144 | jekyll-seo-tag (~> 2.0) 145 | jekyll-theme-dinky (0.1.0) 146 | jekyll (~> 3.5) 147 | jekyll-seo-tag (~> 2.0) 148 | jekyll-theme-hacker (0.1.0) 149 | jekyll (~> 3.5) 150 | jekyll-seo-tag (~> 2.0) 151 | jekyll-theme-leap-day (0.1.0) 152 | jekyll (~> 3.5) 153 | jekyll-seo-tag (~> 2.0) 154 | jekyll-theme-merlot (0.1.0) 155 | jekyll (~> 3.5) 156 | jekyll-seo-tag (~> 2.0) 157 | jekyll-theme-midnight (0.1.0) 158 | jekyll (~> 3.5) 159 | jekyll-seo-tag (~> 2.0) 160 | jekyll-theme-minimal (0.1.0) 161 | jekyll (~> 3.5) 162 | jekyll-seo-tag (~> 2.0) 163 | jekyll-theme-modernist (0.1.0) 164 | jekyll (~> 3.5) 165 | jekyll-seo-tag (~> 2.0) 166 | jekyll-theme-primer (0.5.2) 167 | jekyll (~> 3.5) 168 | jekyll-github-metadata (~> 2.9) 169 | jekyll-seo-tag (~> 2.2) 170 | jekyll-theme-slate (0.1.0) 171 | jekyll (~> 3.5) 172 | jekyll-seo-tag (~> 2.0) 173 | jekyll-theme-tactile (0.1.0) 174 | jekyll (~> 3.5) 175 | jekyll-seo-tag (~> 2.0) 176 | jekyll-theme-time-machine (0.1.0) 177 | jekyll (~> 3.5) 178 | jekyll-seo-tag (~> 2.0) 179 | jekyll-titles-from-headings (0.5.1) 180 | jekyll (~> 3.3) 181 | jekyll-watch (1.5.1) 182 | listen (~> 3.0) 183 | jemoji (0.9.0) 184 | activesupport (~> 4.0, >= 4.2.9) 185 | gemoji (~> 3.0) 186 | html-pipeline (~> 2.2) 187 | jekyll (~> 3.0) 188 | kramdown (1.16.2) 189 | liquid (4.0.0) 190 | listen (3.1.5) 191 | rb-fsevent (~> 0.9, >= 0.9.4) 192 | rb-inotify (~> 0.9, >= 0.9.7) 193 | ruby_dep (~> 1.2) 194 | mercenary (0.3.6) 195 | mini_portile2 (2.5.0) 196 | minima (2.4.0) 197 | jekyll (~> 3.5) 198 | jekyll-feed (~> 0.9) 199 | jekyll-seo-tag (~> 2.1) 200 | minitest (5.11.3) 201 | multipart-post (2.0.0) 202 | net-dns (0.8.0) 203 | nokogiri (1.11.1) 204 | mini_portile2 (~> 2.5.0) 205 | racc (~> 1.4) 206 | octokit (4.8.0) 207 | sawyer (~> 0.8.0, >= 0.5.3) 208 | pathutil (0.16.1) 209 | forwardable-extended (~> 2.6) 210 | public_suffix (2.0.5) 211 | racc (1.5.2) 212 | rb-fsevent (0.10.3) 213 | rb-inotify (0.9.10) 214 | ffi (>= 0.5.0, < 2) 215 | rouge (2.2.1) 216 | ruby-enum (0.7.2) 217 | i18n 218 | ruby_dep (1.5.0) 219 | rubyzip (2.0.0) 220 | safe_yaml (1.0.4) 221 | sass (3.5.5) 222 | sass-listen (~> 4.0.0) 223 | sass-listen (4.0.0) 224 | rb-fsevent (~> 0.9, >= 0.9.4) 225 | rb-inotify (~> 0.9, >= 0.9.7) 226 | sawyer (0.8.1) 227 | addressable (>= 2.3.5, < 2.6) 228 | faraday (~> 0.8, < 1.0) 229 | terminal-table (1.8.0) 230 | unicode-display_width (~> 1.1, >= 1.1.1) 231 | thread_safe (0.3.6) 232 | typhoeus (1.3.0) 233 | ethon (>= 0.9.0) 234 | tzinfo (1.2.5) 235 | thread_safe (~> 0.1) 236 | unicode-display_width (1.3.0) 237 | 238 | PLATFORMS 239 | ruby 240 | 241 | DEPENDENCIES 242 | github-pages 243 | jekyll-feed (~> 0.6) 244 | tzinfo-data 245 | 246 | BUNDLED WITH 247 | 1.16.1 248 | -------------------------------------------------------------------------------- /docs/_api/emit.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Emit" 4 | order: 4 5 | --- 6 | # Emit 7 | 8 | An `emit` is a method of broadcasting an event to the rest of the system which can be listened to by a [listener][listen]. They're synonymous with a distributed version of Node's `EventEmitter`, with the added benefit that they also buffer events for listeners currently offline; emissions never receive responses and so are persisted, meaning a service that has missed X emissions can start up and will be able to process the backlog. 9 | 10 | This makes emissions a very good fit for things that should happen following an event. For instance, sending a welcome email when a user is created, or running image processing after an image is uploaded. 11 | 12 | {% highlight js %} 13 | // set up an emission to let the system know 14 | // a user has been created 15 | const emitUserCreated = remit.emit('user.created') 16 | 17 | // use it again and again! 18 | emitUserCreated({ id: 123, name: 'Jane' }) 19 | emitUserCreated({ id: 456, name: 'John' }) 20 | {% endhighlight %} 21 | 22 | ### Create and send an emission 23 | 24 | `remit.emit(event[, options])` returns a new emitter that will emit data to any interested [listeners][listen], dictated by `event`. The best practice for emissions is to create them once and reuse them. Creation returns a function which, when run, returns a promise that's either resolved or rejected depending on whether the emission successfully sent. 25 | 26 | {% highlight.js %} 27 | // create an emission 28 | const emitUserCreated = remit.request('user.created') 29 | 30 | // send the emission 31 | try { 32 | await emitUserCreated({ id: 123, name: 'Jane' }) 33 | } catch (e) { 34 | // emitting returned an error for some reason 35 | } 36 | 37 | // the `send()` function is the same as running the emitter directly 38 | await emitUserCreated.send({ id: 456, name: 'John' }) 39 | {% endhighlight %} 40 | 41 | The data sent can be anything that plays nicely with [JSON.stringify][json-stringify]. If `data` is not defined, `null` is sent (`undefined` cannot be parsed into JSON). 42 | 43 | If `options` are defined after `data` when _emitting_, the options only apply to that single emission and have no effect on the emitter as a whole. 44 | 45 | ### Set options 46 | 47 | `emit.options(options)` allows you to set the current options for the emission. This can be done at any point in an `emit`ter's life but will not affect emissions that have already been sent. 48 | 49 | {% highlight js %} 50 | const emitUserCreated = remit 51 | .emit('user.created') 52 | .options({ 53 | 54 | }) 55 | 56 | // can also set on creation; `event` is required 57 | const emitPostCreated = remit.emit({ 58 | event: 'post.created', 59 | 60 | }) 61 | {% endhighlight %} 62 | 63 | Available options: 64 | 65 | | Option | Type | Required | Default | Description | 66 | | ------------------------------------------------ | 67 | | `event` | _string_ | yes | | Only required if _creating_ an emitter with an options block. | 68 | | `priority` | _integer_ | | `0` | Can be an integer between `0` and `10`. Higher priority emissions will go to the front of queues before lower priority emissions. | 69 | | `delay` | _integer_/_string_/_Date_ | | | Delay an emission for a minimum amount of time. Either an _integer_ (milliseconds), a [zeit/ms](https://npm.im/ms) _string_, or a _Date_. See Delaying/scheduling below for more information. | 70 | 71 | Returns a reference to the `emit`ter so that calls can be chained. 72 | 73 | ### Delaying/scheduling 74 | 75 | You can delay emissions for an amount of time. They'll be held in a separate messaging queue until they're released, being pushed as normal to the relevant listeners as if had just been emitted. 76 | 77 | This is a good alternative to methods like `cron`. 78 | 79 | You can provide either an _integer_ (milliseconds), a [zeit/ms](https://npm.im/ms) _string_, or a _Date_. 80 | 81 | ### Add listeners 82 | 83 | `emit.on` can be used to register listeners for events specific to a created emitter. See the [Events][events] page for more information on the events emitted. 84 | 85 | Returns a reference to the `emit`ter so that calls can be chained. 86 | 87 | ### Wait for an emitter to be ready 88 | 89 | No setup is needed to set up an emitter, but as a future-proofing measure you can still use an `emit.ready()` promise to check that it's ready to go. 90 | 91 | {% highlight js %} 92 | const emitUserCreated = await remit 93 | .emit('user.created') 94 | .ready() 95 | 96 | // ready to emit 97 | {% endhighlight %} 98 | 99 | Returns a reference to the `emit`ter so that calls can be chained. 100 | -------------------------------------------------------------------------------- /docs/_api/endpoint.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Endpoint" 4 | order: 3 5 | --- 6 | 7 | # Endpoint 8 | 9 | An `endpoint` is a way of providing a function accessible using a [request][request]. A single request will only ever be routed to a single endpoint. 10 | 11 | {% highlight js %} 12 | const logRequest = await remit 13 | .endpoint('log.request', console.log) 14 | .start() 15 | {% endhighlight %} 16 | 17 | ### Create and start an endpoint 18 | 19 | `remit.endpoint(event[, ...handlers])` creates a new endpoint that responds to [requests][request] to `event`. Options can be passed in straight away by passing an object in place of `event`. If an object is passed, the `event` key is required. See `endpoint.options` for available options. 20 | 21 | Handlers are a list of functions that will process the incoming data and return a result. For more information on handlers for both endpoints and listeners, see the [Handlers][handlers] guide. 22 | 23 | {% highlight js %} 24 | // create and start an endpoint 25 | const getUserByIdEndpoint = remit 26 | .endpoint('user.getById', (event) => { 27 | if (!event.data.id) throw new Error('No ID provided to retrieve!') 28 | const { id } = event.data 29 | 30 | return getUserSomehow(id) 31 | }) 32 | .start() 33 | 34 | // Or just a simple value return 35 | const sayHi = remit 36 | .endpoint('say.hi', 'Hi!') 37 | .start() 38 | {% endhighlight %} 39 | 40 | ### Set options 41 | 42 | ### Pause and resume 43 | 44 | ### Add listeners 45 | 46 | `endpoint.on` can be used to register listeners for events specific to a created endpoint. See the [Events][events] page for more information on the events emitted. 47 | 48 | Returns a reference to the `endpoint` so that calls can be chained. 49 | -------------------------------------------------------------------------------- /docs/_api/listen.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Listen" 4 | order: 5 5 | --- 6 | -------------------------------------------------------------------------------- /docs/_api/remit.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Remit" 4 | order: 1 5 | --- 6 | -------------------------------------------------------------------------------- /docs/_api/request.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Request" 4 | order: 2 5 | --- 6 | # Request 7 | 8 | A `request` is a method of retrieving data from an [endpoint][endpoint]. It expects a response and therefore won't be queued. If an endpoint is not available to answer the request within a specified time, it will time out. 9 | 10 | The best practice for creating requests is to create them once and reuse. 11 | 12 | {% highlight js %} 13 | // set up a request to get users by their ID 14 | const getUser = remit.request('user.getById') 15 | 16 | // use it again and again! 17 | const user123 = await getUser(123) 18 | const user456 = await getUser(456) 19 | {% endhighlight %} 20 | 21 | ### Create and send a request 22 | 23 | `remit.request(event[, options])` creates a new requester for data from an [endpoint][endpoint] dictated by `event`. Options can be passed in straight away by passing an object in place of `event`. If an object is passed, the `event` key is required. See `request.options` for available options. 24 | 25 | The best practice for requests is to create them once and reuse them. Creation returns a function which, when run, returns a promise that's either resolved or rejected depending on whether the request succeeded or failed. 26 | 27 | {% highlight js %} 28 | // create a request 29 | const getUser = remit.request('user.getById') 30 | 31 | // send the request, getting user '123' 32 | try { 33 | var user = await getUser(123) 34 | } catch (e) { 35 | // request timed out or returned an error 36 | } 37 | 38 | // the `send()` function is the same as running the request directly 39 | const user = await getUser.send(456) 40 | {% endhighlight %} 41 | 42 | `data` can be anything that plays nicely with [JSON.stringify][json-stringify]. If `data` is not defined, `null` is sent (`undefined` cannot be parsed into JSON). 43 | 44 | `options`, if defined, only apply to that single sent request and have no effect on the `request` as a whole. 45 | 46 | ### Set options 47 | 48 | `request.options(options)` allows you to set the current options for the request. This can be done at any point in a `request`'s life but will not affect requests that have already been sent. 49 | 50 | {% highlight js %} 51 | const getUser = remit 52 | .request('user.getById') 53 | .options({ 54 | timeout: 5000 55 | }) 56 | 57 | // can also set on creation; `event` is required 58 | const getPost = remit.request({ 59 | event: 'post.getById', 60 | priority: 8 61 | }) 62 | {% endhighlight %} 63 | 64 | Available options: 65 | 66 | | Option | Type | Required | Default | Description | 67 | | ------------------------------------------------ | 68 | | `event` | _string_ | yes | | Only required if _creating_ a request with an options block. | 69 | | `timeout` | _string_ or _integer_ | | `30s` | Setting 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". Supports an integer representing milliseconds or a [zeit/ms](https://github.com/zeit/ms)-compatible string. | 70 | | `priority` | _integer_ | | `0` | Can be an integer between `0` and `10`. Higher priority requests will go to the front of queues over lower priority requests. | 71 | 72 | Returns a reference to the `request` so that calls can be chained. 73 | 74 | ### Set a fallback 75 | 76 | `request.fallback(data)` 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. 77 | 78 | {% highlight js %} 79 | const listUsers = remit 80 | .request('user.list') 81 | .fallback([]) 82 | .on('error', console.error) 83 | {% endhighlight %} 84 | 85 | While the fallback will be returned in place of an error, the `request` will still emit an `error` event, so it's good practice to log that to see what's going wrong. 86 | 87 | The fallback can be changed at any point in a `request`'s life and can be unset by passing no arguments to the function. 88 | 89 | Returns a reference to the `request` so that calls can be chained. 90 | 91 | ### Add listeners 92 | 93 | `request.on` can be used to register listeners for events specific to a created request. See the [Events][events] page for more information on the events emitted. 94 | 95 | Returns a reference to the `request` so that calls can be chained. 96 | 97 | ### Wait for a request to be ready 98 | 99 | When a request is created, it needs a short amount of time to set up. This is part of the reason why best practice dictates that you reuse requests rather than creating new ones every time (see [Internals][internals]). 100 | 101 | To check when this is done, you can use `request.ready()`, which returns a promise which resolves when the request is ready to make calls. 102 | 103 | {% highlight js %} 104 | const getUser = await remit 105 | .request('user.getById') 106 | .ready() 107 | 108 | // ready to make calls 109 | {% endhighlight %} 110 | 111 | Any calls made before this promise is resolved will be queued and sent when it's ready. 112 | 113 | Returns a reference to the `request` so that calls can be chained. 114 | -------------------------------------------------------------------------------- /docs/_change/v2.3.3.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: v2.3.3 4 | --- 5 | Oop 6 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: Remit documentation 2 | email: jpwilliamsphotography@gmail.com 3 | description: >- # this means to ignore newlines until "baseurl:" 4 | RabbitMQ-backed microservices supporting RPC, pubsub, automatic service discovery and scaling with no code changes. 5 | baseurl: "/remit" # the subpath of your site, e.g. /blog 6 | url: "" # the base hostname & protocol for your site, e.g. http://example.com 7 | 8 | sidebar_collections: 9 | - start 10 | - guide 11 | - api 12 | - advanced 13 | - change 14 | 15 | collections: 16 | start: 17 | output: true 18 | permalink: /docs/getting-started/:path 19 | title: Getting started 20 | guide: 21 | output: true 22 | permalink: /docs/guide/:path 23 | title: Guide 24 | api: 25 | output: true 26 | permalink: /docs/api/:path 27 | title: API 28 | advanced: 29 | output: true 30 | permalink: /docs/advanced/:path 31 | title: Advanced 32 | change: 33 | output: true 34 | permalink: /changelog/:path 35 | title: Changelog 36 | 37 | # Build settings 38 | markdown: kramdown 39 | 40 | sass: 41 | style: compressed 42 | 43 | # Exclude from processing. 44 | # The following items will not be processed, by default. Create a custom list 45 | # to override the default setting. 46 | # exclude: 47 | # - Gemfile 48 | # - Gemfile.lock 49 | # - node_modules 50 | # - vendor/bundle/ 51 | # - vendor/cache/ 52 | # - vendor/gems/ 53 | # - vendor/ruby/ 54 | -------------------------------------------------------------------------------- /docs/_guide/concepts.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Concepts 4 | order: 1 5 | --- 6 | # Concepts 7 | 8 | ### Publish Subscribe 9 | 10 | ### Request Reply 11 | 12 | ### Queueing 13 | 14 | ### Consumer groups and scaling 15 | -------------------------------------------------------------------------------- /docs/_guide/consumer-groups.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Consumer groups 4 | order: 5 5 | --- 6 | -------------------------------------------------------------------------------- /docs/_guide/events.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Events 4 | order: 2 5 | --- 6 | -------------------------------------------------------------------------------- /docs/_guide/handlers.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Handlers 4 | order: 3 5 | --- 6 | -------------------------------------------------------------------------------- /docs/_guide/internals.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Internals 4 | order: 7 5 | --- 6 | - the fact creating a request must create a consumer/producer channel, so it's better to re-use 7 | - what happens when I create an endpoint etc 8 | -------------------------------------------------------------------------------- /docs/_guide/middleware.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Middleware 4 | order: 4 5 | --- 6 | -------------------------------------------------------------------------------- /docs/_guide/tracing.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Tracing 4 | order: 6 5 | --- 6 | # OpenTracing 7 | 8 | ![Jaeger Tracing](https://user-images.githubusercontent.com/1736957/41066405-9bf0808e-69d9-11e8-8d2a-b4704ca2731a.png) 9 | 10 | Remit supports [OpenTracing](https://opentracing.io)-compatible tracers, pushing data to any compatible backend. More information on the OpenTracing API can be found at [https://opentracing.io](https://opentracing.io). 11 | 12 | Officially supported tracers that provide Node.js clients are currently: 13 | 14 | - [Jaeger](https://www.jaegertracing.io) 15 | - [LightStep](http://lightstep.com/) 16 | - [Instana](https://www.instana.com/) 17 | - [Datadog](https://www.datadoghq.com/apm/) 18 | 19 | # Adding a tracer 20 | 21 | Using a tracer with Remit is exceedingly simple. When instantiating Remit, simply pass in a `tracer` option. The example below uses the popular [jaegertracing/jaeger-client-node](https://github.com/jaegertracing/jaeger-client-node). 22 | 23 | {% highlight js %} 24 | const Remit = require('@jpwilliams/remit') 25 | const { initTracer } = require('jaeger-client') 26 | const serviceName = 'my-traced-service' 27 | 28 | // most tracers allow some configuration to choose when to 29 | // take trace samples. This config will ensure traces 30 | // are always created. 31 | const tracer = initTracer({ 32 | serviceName, 33 | sampler: { 34 | type: 'const', 35 | param: 1 36 | } 37 | }) 38 | 39 | const remit = Remit({ 40 | name: serviceName, 41 | tracer 42 | }) 43 | {% endhighlight %} 44 | 45 | All calls for this Remit instance will now be traced! Great! 46 | 47 | # Namespaces 48 | 49 | If attempting to trace multiple libraries/frameworks, you'll need to have them cooperating with each-other to make relevant traces. While the method to perform this [hasn't yet been nailed down](https://github.com/opentracing/specification/issues/23), Remit will provide a solution that's most likely in line with the resulting OpenTracing specification changes. 50 | 51 | We currently use [jeff-lewis/cls-hooked](https://github.com/jeff-lewis/cls-hooked) to infer span contexts between Remit calls. This has worked well even previous to the introduction of OpenTracing, so we'll use it again here. 52 | 53 | Remit allows you to pass in a `namespace` upon instantiation, so you can have `get`/`set` access to the namespace providing the relevant contexts. If you don't know how these contexts work, I strongly suggest you read the [jeff-lewis/cls-hooked](https://github.com/jeff-lewis/cls-hooked) docs and get a grip on namespaces and contexts before use. 54 | 55 | {% highlight js %} 56 | const Remit = require('@jpwilliams/remit') 57 | const { Tracer } = require('opentracing') 58 | const cls = require('cls-hooked') 59 | 60 | const tracer = new Tracer() 61 | const namespace = cls.createNamespace('tracing') 62 | const remit = Remit({ namespace, tracer }) 63 | 64 | // Internally, Remit uses the 'context' key to store the current 65 | // span context, so set this to update it. 66 | const span = tracer.startSpan('my-http-request') 67 | namespace.set('context', span.context()) 68 | {% endhighlight %} 69 | 70 | This `namespace` API is currently seen as _experimental_ and __will change without a major version bump upon the OpenTracing specificaton decision__. 71 | -------------------------------------------------------------------------------- /docs/_includes/body.html: -------------------------------------------------------------------------------- 1 |
2 | {% include header.html %} 3 | 4 |
5 | {% include sidebar.html %} 6 | {% include content.html %} 7 |
8 |
9 | -------------------------------------------------------------------------------- /docs/_includes/content.html: -------------------------------------------------------------------------------- 1 |
2 | {{ content }} 3 |
4 | -------------------------------------------------------------------------------- /docs/_includes/critical.scss: -------------------------------------------------------------------------------- 1 | html { 2 | // ensure scroll bar always appears so that 3 | // it appearing/disappearing doesn't adjust 4 | // page width (and therefore content position) 5 | // between pages. 6 | overflow-y: scroll; 7 | } 8 | 9 | html, body { 10 | padding: 0; 11 | margin: 0; 12 | width: 100%; 13 | border-top: 2px solid #172b4d; 14 | color: #172b4d; 15 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 16 | font-size: 16px; 17 | text-rendering: optimizeLegibility; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | 22 | a { 23 | color: #0096e2; 24 | text-decoration: none; 25 | } 26 | 27 | .wrapper { 28 | width: 1000px; 29 | max-width: 100%; 30 | margin: 0 auto; 31 | padding: 0 2rem; 32 | box-sizing: border-box; 33 | 34 | .innerWrapper { 35 | margin: 2rem 0; 36 | display: -webkit-box; 37 | flex-direction: row; 38 | } 39 | } 40 | 41 | .header { 42 | border-bottom: 2px solid grey; 43 | padding: 1rem 0; 44 | 45 | .title { 46 | margin: 0; 47 | } 48 | } 49 | 50 | .sidebar { 51 | width: calc(200px - 2rem); 52 | max-width: 30%; 53 | padding-right: 2rem; 54 | will-change: min-height; 55 | 56 | .sidebar-inner { 57 | transform: translate(0, 0); 58 | transform: translate3d(0, 0, 0); 59 | will-change: position, transform; 60 | } 61 | 62 | .sidebar-header { 63 | text-transform: uppercase; 64 | font-weight: 600; 65 | color: #888888; 66 | margin-bottom: 0.5rem; 67 | } 68 | 69 | .sidebar-list { 70 | margin: 0 0 2rem; 71 | list-style: none; 72 | padding: 0; 73 | 74 | &:last-child { 75 | margin: 0; 76 | } 77 | 78 | .sidebar-item { 79 | &.active { 80 | font-weight: bold; 81 | border-right: 2px solid #0096e2; 82 | } 83 | } 84 | } 85 | } 86 | 87 | .content { 88 | -webkit-box-flex: 1; 89 | 90 | img { 91 | width: 100%; 92 | } 93 | 94 | h1, h2, h3, h4, h5 { 95 | position: relative; 96 | 97 | &:first-child { 98 | margin-top: 0; 99 | } 100 | 101 | &:before { 102 | content: '#'; 103 | position: absolute; 104 | left: -1em; 105 | color: #0096e2; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /docs/_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% capture title %}{% if page.title %}{{ page.title }} - {% endif %}{{ site.title }}{% endcapture %} 11 | {% capture description %}{% if page.description %}{{ page.description}}{% else %}{{ site.description }}{% endif %}{% endcapture %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {{ title }} 20 | 21 | 22 | 23 | 24 | {% capture criticalStyles %} 25 | {% include critical.scss %} 26 | {% endcapture %} 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/_includes/header.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | {{ site.title }} 4 |

5 |
6 | -------------------------------------------------------------------------------- /docs/_includes/sidebar.html: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /docs/_includes/sticky-sidebar.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/_includes/sw.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include head.html %} 5 | 6 | 7 | 8 | {% include body.html %} 9 | {% include sw.html %} 10 | {% include sticky-sidebar.html %} 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/_posts/2018-03-15-welcome-to-jekyll.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Welcome to Jekyll!" 4 | date: 2018-03-15 21:38:27 +0000 5 | categories: changelog 6 | --- 7 | You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated. 8 | 9 | To add new posts, simply add a file in the `_posts` directory that follows the convention `YYYY-MM-DD-name-of-post.ext` and includes the necessary front matter. Take a look at the source for this post to get an idea about how it works. 10 | 11 | Jekyll also offers powerful support for code snippets: 12 | 13 | {% highlight ruby %} 14 | def print_hi(name) 15 | puts "Hi, #{name}" 16 | end 17 | print_hi('Tom') 18 | #=> prints 'Hi, Tom' to STDOUT. 19 | {% endhighlight %} 20 | 21 | Check out the [Jekyll docs][jekyll-docs] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll Talk][jekyll-talk]. 22 | 23 | [jekyll-docs]: https://jekyllrb.com/docs/home 24 | [jekyll-gh]: https://github.com/jekyll/jekyll 25 | [jekyll-talk]: https://talk.jekyllrb.com/ 26 | -------------------------------------------------------------------------------- /docs/_start/installing.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Installing" 4 | order: 1 5 | --- 6 | # Installing 7 | 8 | Remit relies on [RabbitMQ][rabbitmq] to be the glue between your microservices. They also provide [easy instructions for installing RabbitMQ][rabbitmq-download] on a multitude of platforms. Here, we'll assume you're running on a MacOS and install it locally using brew. 9 | 10 | {% highlight bash %} 11 | brew install rabbitmq 12 | brew services start rabbitmq 13 | {% endhighlight %} 14 | 15 | Next, in whatever Node project you're running, use `npm`, `yarn` or a similar package manager to install Remit: 16 | 17 | {% highlight bash %} 18 | npm install @jpwilliams/remit --save 19 | {% endhighlight %} 20 | 21 | Done! 22 | 23 | Next: [Simple example]({{ site.baseurl }}{% link _start/simple-example.md %}) 24 | 25 | [rabbitmq]: https://www.rabbitmq.com 26 | [rabbitmq-download]: https://www.rabbitmq.com/download.html 27 | -------------------------------------------------------------------------------- /docs/_start/simple-example.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Simple example" 4 | order: 2 5 | --- 6 | # Simple example 7 | 8 | A common interaction between services is a request and a response. We'll test that here by using a Remit `request` and `endpoint`. 9 | 10 | {% highlight js %} 11 | // endpoint.js 12 | const remit = require('@jpwilliams/remit')() 13 | const endpoint = remit 14 | .endpoint('hello') 15 | .handler(event => `Hello, ${event.data.name}!`) 16 | .start() 17 | 18 | // request.js 19 | const remit = require('@jpwilliams/remit')() 20 | const sayHello = remit.request('hello') 21 | sayHello({name: 'Jack'}).then(console.log) 22 | {% endhighlight %} 23 | 24 | Here, we create two files: an endpoint called `'hello'` which, when hit, returns `'Hello, NAME!'` and a requester which will hit the endpoint with some data and log the result. 25 | 26 | Boom. Done. 27 | 28 | If you're happy, take a look at the [Concepts]({{ site.baseurl }}{% link _guide/concepts.md %}) page to get affiliated with the four base types that Remit provides you with. If you'd like a more thorough explanation of the above, read on. 29 | 30 | ### Create a project 31 | 32 | > We'll assume you have a local RabbitMQ running (following the [installation instructions]({{ site.baseurl }}{% link _start/installing.md %})). 33 | 34 | First, let's create a new project and install Remit. In the terminal: 35 | 36 | {% highlight bash %} 37 | mkdir remit-example 38 | cd remit-example 39 | npm init -y 40 | npm install @jpwilliams/remit --save 41 | {% endhighlight %} 42 | 43 | 44 | ### Create an endpoint 45 | 46 | Sorted. Now let's create a new file called `endpoint.js`: 47 | 48 | {% highlight js %} 49 | // endpoint.js 50 | const Remit = require('@jpwilliams/remit') // import remit 51 | const remit = Remit() // connect to remit 52 | 53 | // create a new endpoint 54 | const endpoint = remit 55 | .endpoint('hello') // name it "hello" 56 | .handler('Hello, world!') // return "Hello, world!" when its hit 57 | .start() // start it! 58 | .then(() => console.log('Ready!')) // log when it's booted up 59 | {% endhighlight %} 60 | 61 | Super simple! We've got a file ready that boots an endpoint! 62 | 63 | > When the endpoint boots, Remit will create a RabbitMQ "queue" for incoming messages. Requests will place a message in the endpoint's queue and provide an address of sorts to receive the reply on. 64 | 65 | ### Create a request 66 | 67 | Let's now create another file called `request.js` which we'll use to send a request to our `'hello'` endpoint: 68 | 69 | {% highlight js %} 70 | // request.js 71 | const Remit = require('@jpwilliams/remit') // import remit 72 | const remit = Remit() // connect to remit 73 | 74 | const sayHello = remit.request('hello') // set up a request that hits the "hello" endpoint 75 | sayHello().then(console.log) // send the request and log what comes back 76 | {% endhighlight %} 77 | 78 | When that file is run, it'll send a request to our `'hello'` endpoint. 79 | 80 | > When the requester boots, Remit creates a temporary "reply" queue for incoming replies. Because of this small overhead, it's best to create a single request and re-use it multiple times. 81 | 82 | Awesome. Let's give it a try. 83 | 84 | ### Run it! 85 | 86 | {% highlight bash %} 87 | # Terminal A 88 | $ node endpoint.js 89 | Ready! 90 | {% endhighlight %} 91 | 92 | Great. Our endpoint is booted! Now let's send a request! 93 | 94 | {% highlight bash %} 95 | # Terminal B 96 | $ node request.js 97 | Hello, world! 98 | {% endhighlight %} 99 | 100 | Woo! We made a request from one Node process, through Remit, to another Node process and back again! As long as these processes are connected to Remit, they can find eachother and communicate! 101 | 102 | ### Improvements 103 | 104 | Right now, this returns a fixed piece of data, `'Hello, world!'`, but let's change that to instead use the data we've been sent and say hello to a particular person. We'll adjust our `endpoint.js` file to use a function for its handler instead. 105 | 106 | {% highlight diff %} 107 | const endpoint = remit 108 | .endpoint('hello') 109 | - .handler('Hello, world!') 110 | + .handler(event => `Hello, ${event.data.name}!`) 111 | .start() 112 | .then(() => console.log('Ready!')) 113 | {% endhighlight %} 114 | 115 | > `event` is a useful object. Most importantly, we can extract data sent by requests. Here, we're grabbing the `name` property from the incoming `data` and using it to form what we return. 116 | 117 | Let's change the request too so that we're sending `name` now. 118 | 119 | {% highlight diff %} 120 | const sayHello = remit.request('hello') 121 | - sayHello().then(console.log) 122 | + sayHello({ name: 'Jack '}).then(console.log) 123 | {% endhighlight %} 124 | 125 | Cool. We'll give that a try: 126 | 127 | {% highlight bash %} 128 | # Terminal A 129 | $ node endpoint.js 130 | Ready! 131 | 132 | # Terminal B 133 | $ node request.js 134 | Hello, Jack! 135 | {% endhighlight %} 136 | 137 | Woohoo! 138 | 139 | Next: [Concepts]({{ site.baseurl }}{% link _guide/concepts.md %}) 140 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: About 4 | permalink: /about/ 5 | --- 6 | Examples are a powerful, visual way of demonstrating functionality, so let's use one: creating a user. When a user's created, I also want to send them a welcome email and notify any of their Facebook friends on the platform that they've registered. 7 | 8 | {% highlight js %} 9 | // a user service 10 | const remit = require('remit')({ 11 | name: 'User Service' 12 | }) 13 | 14 | // set up an emitter we can use to emit that we've created 15 | // a new user. 16 | const emitUserCreated = remit.emit('user.created') 17 | 18 | // set up an endpoint that we can hit to create a user 19 | remit 20 | .endpoint('user.create') 21 | .handler(createUser) 22 | .start() 23 | 24 | async function createUser (event) { 25 | const userToCreate = { 26 | name: event.data.user.name, 27 | email: event.data.user.email 28 | } 29 | 30 | const user = createUserInDb(userToCreate) 31 | emitUserCeated(user) 32 | 33 | return user 34 | } 35 | {% endhighlight %} 36 | 37 | --- 38 | 39 | {% highlight js %} 40 | // an API. 41 | // we'll assume this is an HTTP REST API that's set up to 42 | // recevie requests to create a user. It's going to make 43 | // a request to our Remit endpoint. 44 | const remit = require('remit')({ 45 | name: 'The API' 46 | }) 47 | 48 | const createUser = remit.request('user.create') 49 | 50 | api.post('/user', async (req, res, next) => { 51 | const user = await createUser(req.body) 52 | 53 | return res.status(200).send(user) 54 | }) 55 | {% endhighlight %} 56 | 57 | --- 58 | 59 | {% highlight js %} 60 | // An emailing service. All this service does is listen 61 | // for events and send out relevant emails. One of those 62 | // events is user creation. 63 | // 64 | // Listeners are durable; even if our listener dies, it 65 | // will queue up requests for when it's back online. This 66 | // is perfect for services which must process every event, 67 | // like emails or thumbnail creation. 68 | const remit = require('remit')({ 69 | name: 'Emailer Service' 70 | }) 71 | 72 | remit 73 | .listen('user.created') 74 | .handler(sendWelcomeEmail) 75 | .start() 76 | 77 | async function sendWelcomeEmail (event) { 78 | const { name, email } = event.data 79 | sendWelcomeEmailTo(email, name) 80 | } 81 | {% endhighlight %} 82 | 83 | --- 84 | 85 | {% highlight js %} 86 | // Facebook service. 87 | // Here we also listen to the user creation event, using it 88 | // to find and notify their friends that they've registered. 89 | // 90 | // Emitted events (such as 'user.created') go to all registered 91 | // listeners, meaning everyone that's interested gets notified. 92 | const remit = require('remit')({ 93 | name: 'Facebook Service' 94 | }) 95 | 96 | remit 97 | .listen('user.created') 98 | .handler(checkFriends) 99 | .start() 100 | 101 | const getFriendsOnNetwork = remit.request('facebook.getFriends') 102 | const notifyFriend = remit.request('otherservice.notify') 103 | 104 | async function checkFriends (event) { 105 | const { id } = event.data 106 | const friendsOnNetwork = await getFriendsOnNetwork(id) 107 | 108 | if (friendsOnNetwork.length) { 109 | return Promise.all(friendsOnNetwork.map((friend) => { 110 | return notifyFriend(friend) 111 | })) 112 | } 113 | } 114 | {% endhighlight %} 115 | -------------------------------------------------------------------------------- /docs/css/monokai.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Code formatting 3 | */ 4 | pre, 5 | code { 6 | font-size: 0.82rem; 7 | font-family: Inconsolata, Monaco, Consolas, "Courier New", Courier, monospace; 8 | } 9 | 10 | code { 11 | padding: 1px 5px; 12 | border-radius: 0.2rem; 13 | border: 1px solid #b4b4b4; 14 | } 15 | 16 | pre { 17 | border-radius: 0.8rem; 18 | padding: 1.4rem; 19 | overflow-x: auto; 20 | tab-size: 4; 21 | } 22 | 23 | pre > code { 24 | color: white; 25 | border: 0; 26 | padding-right: 0; 27 | padding-left: 0; 28 | } 29 | 30 | figure { 31 | margin: 0; 32 | } 33 | 34 | .highlight pre { background-color: #2d3048; } 35 | .highlight .hll { background-color: #2d3048; } 36 | .highlight .c { color: #75715e } /* Comment */ 37 | .highlight .err { color: #960050; background-color: #1e0010 } /* Error */ 38 | .highlight .k { color: #66d9ef } /* Keyword */ 39 | .highlight .l { color: #ae81ff } /* Literal */ 40 | .highlight .n { color: #f8f8f2 } /* Name */ 41 | .highlight .o { color: #f92672 } /* Operator */ 42 | .highlight .p { color: #f8f8f2 } /* Punctuation */ 43 | .highlight .cm { color: #75715e } /* Comment.Multiline */ 44 | .highlight .cp { color: #75715e } /* Comment.Preproc */ 45 | .highlight .c1 { color: #75715e } /* Comment.Single */ 46 | .highlight .cs { color: #75715e } /* Comment.Special */ 47 | .highlight .ge { font-style: italic } /* Generic.Emph */ 48 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 49 | .highlight .kc { color: #66d9ef } /* Keyword.Constant */ 50 | .highlight .kd { color: #66d9ef } /* Keyword.Declaration */ 51 | .highlight .kn { color: #f92672 } /* Keyword.Namespace */ 52 | .highlight .kp { color: #66d9ef } /* Keyword.Pseudo */ 53 | .highlight .kr { color: #66d9ef } /* Keyword.Reserved */ 54 | .highlight .kt { color: #66d9ef } /* Keyword.Type */ 55 | .highlight .ld { color: #e6db74 } /* Literal.Date */ 56 | .highlight .m { color: #ae81ff } /* Literal.Number */ 57 | .highlight .s { color: #e6db74 } /* Literal.String */ 58 | .highlight .na { color: #a6e22e } /* Name.Attribute */ 59 | .highlight .nb { color: #f8f8f2 } /* Name.Builtin */ 60 | .highlight .nc { color: #a6e22e } /* Name.Class */ 61 | .highlight .no { color: #66d9ef } /* Name.Constant */ 62 | .highlight .nd { color: #a6e22e } /* Name.Decorator */ 63 | .highlight .ni { color: #f8f8f2 } /* Name.Entity */ 64 | .highlight .ne { color: #a6e22e } /* Name.Exception */ 65 | .highlight .nf { color: #a6e22e } /* Name.Function */ 66 | .highlight .nl { color: #f8f8f2 } /* Name.Label */ 67 | .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ 68 | .highlight .nx { color: #a6e22e } /* Name.Other */ 69 | .highlight .py { color: #f8f8f2 } /* Name.Property */ 70 | .highlight .nt { color: #f92672 } /* Name.Tag */ 71 | .highlight .nv { color: #f8f8f2 } /* Name.Variable */ 72 | .highlight .ow { color: #f92672 } /* Operator.Word */ 73 | .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ 74 | .highlight .mf { color: #ae81ff } /* Literal.Number.Float */ 75 | .highlight .mh { color: #ae81ff } /* Literal.Number.Hex */ 76 | .highlight .mi { color: #ae81ff } /* Literal.Number.Integer */ 77 | .highlight .mo { color: #ae81ff } /* Literal.Number.Oct */ 78 | .highlight .sb { color: #e6db74 } /* Literal.String.Backtick */ 79 | .highlight .sc { color: #e6db74 } /* Literal.String.Char */ 80 | .highlight .sd { color: #e6db74 } /* Literal.String.Doc */ 81 | .highlight .s2 { color: #e6db74 } /* Literal.String.Double */ 82 | .highlight .se { color: #ae81ff } /* Literal.String.Escape */ 83 | .highlight .sh { color: #e6db74 } /* Literal.String.Heredoc */ 84 | .highlight .si { color: #e6db74 } /* Literal.String.Interpol */ 85 | .highlight .sx { color: #e6db74 } /* Literal.String.Other */ 86 | .highlight .sr { color: #e6db74 } /* Literal.String.Regex */ 87 | .highlight .s1 { color: #e6db74 } /* Literal.String.Single */ 88 | .highlight .ss { color: #e6db74 } /* Literal.String.Symbol */ 89 | .highlight .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ 90 | .highlight .vc { color: #f8f8f2 } /* Name.Variable.Class */ 91 | .highlight .vg { color: #f8f8f2 } /* Name.Variable.Global */ 92 | .highlight .vi { color: #f8f8f2 } /* Name.Variable.Instance */ 93 | .highlight .il { color: #ae81ff } /* Literal.Number.Integer.Long */ 94 | 95 | .highlight .gh { } /* Generic Heading & Diff Header */ 96 | .highlight .gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */ 97 | .highlight .gd { color: #f92672; } /* Generic.Deleted & Diff Deleted */ 98 | .highlight .gi { color: #a6e22e; } /* Generic.Inserted & Diff Inserted */ 99 | -------------------------------------------------------------------------------- /docs/css/syntax.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | // pre { margin: 0; } 4 | pre, code, pre code { 5 | border: none; 6 | border-radius: 0; 7 | background-color: #f9f9f9; 8 | margin: 0; 9 | } 10 | 11 | code { padding: 2px 4px; font-size: 90%; } 12 | 13 | pre code { 14 | padding: 0; 15 | white-space: pre-wrap; 16 | white-space: -moz-pre-wrap; 17 | white-space: -pre-wrap; 18 | white-space: -o-pre-wrap; 19 | word-wrap: break-word; 20 | } 21 | 22 | figure.highlight { margin: 0; padding: 1rem; } 23 | .highlight .hll { background-color: #ffffcc } 24 | .highlight { background: #f9f9f9; } 25 | .highlight .c { color: #888888 } /* Comment */ 26 | .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ 27 | .highlight .k { color: #008800; font-weight: bold } /* Keyword */ 28 | .highlight .cm { color: #888888 } /* Comment.Multiline */ 29 | .highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */ 30 | .highlight .c1 { color: #888888 } /* Comment.Single */ 31 | .highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */ 32 | .highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ 33 | .highlight .ge { font-style: italic } /* Generic.Emph */ 34 | .highlight .gr { color: #aa0000 } /* Generic.Error */ 35 | .highlight .gh { color: #333333 } /* Generic.Heading */ 36 | .highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ 37 | .highlight .go { color: #888888 } /* Generic.Output */ 38 | .highlight .gp { color: #555555 } /* Generic.Prompt */ 39 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 40 | .highlight .gu { color: #666666 } /* Generic.Subheading */ 41 | .highlight .gt { color: #aa0000 } /* Generic.Traceback */ 42 | .highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */ 43 | .highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */ 44 | .highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */ 45 | .highlight .kp { color: #008800 } /* Keyword.Pseudo */ 46 | .highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */ 47 | .highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */ 48 | .highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */ 49 | .highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */ 50 | .highlight .na { color: #336699 } /* Name.Attribute */ 51 | .highlight .nb { color: #003388 } /* Name.Builtin */ 52 | .highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */ 53 | .highlight .no { color: #003366; font-weight: bold } /* Name.Constant */ 54 | .highlight .nd { color: #555555 } /* Name.Decorator */ 55 | .highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */ 56 | .highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */ 57 | .highlight .nl { color: #336699; font-style: italic } /* Name.Label */ 58 | .highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */ 59 | .highlight .py { color: #336699; font-weight: bold } /* Name.Property */ 60 | .highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */ 61 | .highlight .nv { color: #336699 } /* Name.Variable */ 62 | .highlight .ow { color: #008800 } /* Operator.Word */ 63 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 64 | .highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */ 65 | .highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */ 66 | .highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ 67 | .highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */ 68 | .highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ 69 | .highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ 70 | .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ 71 | .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ 72 | .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ 73 | .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ 74 | .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ 75 | .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ 76 | .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ 77 | .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ 78 | .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ 79 | .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ 80 | .highlight .vc { color: #336699 } /* Name.Variable.Class */ 81 | .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ 82 | .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ 83 | .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */ 84 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # You don't need to edit this file, it's empty on purpose. 3 | # Edit theme's home layout instead if you wanna make some changes 4 | # See: https://jekyllrb.com/docs/themes/#overriding-theme-defaults 5 | layout: default 6 | --- 7 | # Remit 8 | 9 | Remit is a RabbitMQ-backed library for building microservices supporting RPC, pubsub, automatic service discovery, tracing and scaling with no code changes. 10 | 11 | {% highlight js %} 12 | // service.js 13 | const endpoint = remit.endpoint('user.get') 14 | 15 | endpoint.handler((event) => { 16 | const user = { name: 'Jack Williams' } 17 | remit.emit('got user', user) 18 | 19 | return user 20 | }) 21 | 22 | endpoint.start() 23 | {% endhighlight %} 24 | 25 | Then in another process... 26 | 27 | {% highlight js %} 28 | // api.js 29 | const getUser = remit.request('user.get') 30 | const user = await getUser(123) 31 | // {"name":"Jack Williams"} 32 | {% endhighlight %} 33 | 34 | Multiple things could then hook in to the `"got user"` event we emitted... 35 | 36 | {% highlight js %} 37 | // listener.js 38 | const listener = remit.listen('got user') 39 | 40 | listener.handler((event) => { 41 | log(`User "${event.data.name}" was requested.`) 42 | }) 43 | 44 | listener.start() 45 | {% endhighlight %} 46 | 47 | ### What is it for? 48 | 49 | Remit is generically designed for creating separated services which respond to or listen to events. An `endpoint` will wait for and respond to individual `request`s and a `listener` will receive all `emit`s across the entire system. 50 | 51 | With that basic model, Remit expands to allow you to: 52 | 53 | - Create "services" that respond to messages 54 | - Trace messages flowing through your system and produce distrubuted stack traces 55 | - Group and scale services automatically 56 | - A/B test 57 | - "Event" your system, creating distrubuted hooks you can use at any time, anywhere 58 | - Easily break apart your system, only creating separate "services" when it suits with minimal code changes 59 | 60 | ### How does it work? 61 | 62 | Remit is a wrapper around the fantastic [RabbitMQ][rabbitmq] message broker and tries to be as easy to use as possible. It handles creating the connections, channels and bindings needed for RabbitMQ and provides a pretty API for interacting with your services and messages. 63 | 64 | [rabbitmq]: https://rabbitmq.com 65 | -------------------------------------------------------------------------------- /docs/js/sticky-sidebar.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * sticky-sidebar - A JavaScript plugin for making smart and high performance. 3 | * @version v3.3.1 4 | * @link https://github.com/abouolia/sticky-sidebar 5 | * @author Ahmed Bouhuolia 6 | * @license The MIT License (MIT) 7 | **/ 8 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.StickySidebar={})}(this,function(t){"use strict";"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;var e,i,n=(function(t,e){(function(t){Object.defineProperty(t,"__esModule",{value:!0});var l,n,e=function(){function n(t,e){for(var i=0;i=t.containerBottom?(t.translateY=t.containerBottom-e,o="CONTAINER-BOTTOM"):i>=t.containerTop&&(t.translateY=i-t.containerTop,o="VIEWPORT-TOP"):t.containerBottom<=n?(t.translateY=t.containerBottom-e,o="CONTAINER-BOTTOM"):e+t.translateY<=n?(t.translateY=n-e,o="VIEWPORT-BOTTOM"):t.containerTop+t.translateY<=i&&0!==t.translateY&&t.maxTranslateY!==t.translateY&&(o="VIEWPORT-UNBOTTOM"),o}},{key:"_getAffixTypeScrollingUp",value:function(){var t=this.dimensions,e=t.sidebarHeight+t.containerTop,i=t.viewportTop+t.topSpacing,n=t.viewportBottom-t.bottomSpacing,o=this.affixedType;return i<=t.translateY+t.containerTop?(t.translateY=i-t.containerTop,o="VIEWPORT-TOP"):t.containerBottom<=n?(t.translateY=t.containerBottom-e,o="CONTAINER-BOTTOM"):this.isSidebarFitsViewport()||t.containerTop<=i&&0!==t.translateY&&t.maxTranslateY!==t.translateY&&(o="VIEWPORT-UNBOTTOM"),o}},{key:"_getStyle",value:function(t){if(void 0!==t){var e={inner:{},outer:{}},i=this.dimensions;switch(t){case"VIEWPORT-TOP":e.inner={position:"fixed",top:i.topSpacing,left:i.sidebarLeft-i.viewportLeft,width:i.sidebarWidth};break;case"VIEWPORT-BOTTOM":e.inner={position:"fixed",top:"auto",left:i.sidebarLeft,bottom:i.bottomSpacing,width:i.sidebarWidth};break;case"CONTAINER-BOTTOM":case"VIEWPORT-UNBOTTOM":var n=this._getTranslate(0,i.translateY+"px");e.inner=n?{transform:n}:{position:"absolute",top:i.translateY,width:i.sidebarWidth}}switch(t){case"VIEWPORT-TOP":case"VIEWPORT-BOTTOM":case"VIEWPORT-UNBOTTOM":case"CONTAINER-BOTTOM":e.outer={height:i.sidebarHeight,position:"relative"}}return e.outer=c.extend({height:"",position:""},e.outer),e.inner=c.extend({position:"relative",top:"",left:"",bottom:"",width:"",transform:""},e.inner),e}}},{key:"stickyPosition",value:function(t){if(!this._breakpoint){t=this._reStyle||t||!1,this.options.topSpacing,this.options.bottomSpacing;var e=this.getAffixType(),i=this._getStyle(e);if((this.affixedType!=e||t)&&e){var n="affix."+e.toLowerCase().replace("viewport-","")+l;for(var o in c.eventTrigger(this.sidebar,n),"STATIC"===e?c.removeClass(this.sidebar,this.options.stickyClass):c.addClass(this.sidebar,this.options.stickyClass),i.outer){var s="number"==typeof i.outer[o]?"px":"";this.sidebar.style[o]=i.outer[o]+s}for(var r in i.inner){var a="number"==typeof i.inner[r]?"px":"";this.sidebarInner.style[r]=i.inner[r]+a}var p="affixed."+e.toLowerCase().replace("viewport-","")+l;c.eventTrigger(this.sidebar,p)}else this._initialized&&(this.sidebarInner.style.left=i.inner.left);this.affixedType=e}}},{key:"_widthBreakpoint",value:function(){window.innerWidth<=this.options.minWidth?(this._breakpoint=!0,this.affixedType="STATIC",this.sidebar.removeAttribute("style"),c.removeClass(this.sidebar,this.options.stickyClass),this.sidebarInner.removeAttribute("style")):this._breakpoint=!1}},{key:"updateSticky",value:function(){var t,e=this,i=0 { 33 | const cache = await caches.open(currentCache) 34 | 35 | return cache.addAll(cachedUrls) 36 | })()) 37 | } 38 | 39 | function onActivate (event) { 40 | event.waitUntil((async () => { 41 | const keys = await caches.keys() 42 | 43 | return Promise.all(keys.map((key) => { 44 | if (key !== currentCache) { 45 | console.log('Deleting old cached data:', key) 46 | 47 | return caches.delete(key) 48 | } 49 | })) 50 | })()) 51 | } 52 | 53 | function onFetch (event) { 54 | const req = event.request 55 | 56 | // don't interrupt calls that aren't GET 57 | if (req.method !== 'GET') { 58 | return 59 | } 60 | 61 | event.respondWith((async () => { 62 | // start getting from network 63 | const fetchRes = fetch(req) 64 | 65 | // make sure the service worker stays alive to cache 66 | // the new content if the fetch succeeds 67 | event.waitUntil((async () => { 68 | try { 69 | const fetchResCopy = (await fetchRes).clone() 70 | const myCache = await caches.open(currentCache) 71 | await myCache.put(req, fetchResCopy) 72 | } catch (err) { 73 | console.warn('Failed to update cache for', req.url,'-', err) 74 | } 75 | })()) 76 | 77 | // if the target is HTML, grab fresh content first, then cached. 78 | // if the target is not HTML, grab cache first, then fresh. 79 | if (req.headers.get('Accept').includes('text/html')) { 80 | try { 81 | return await fetchRes 82 | } catch (err) { 83 | return caches.match(req) 84 | } 85 | } else { 86 | const cacheRes = await caches.match(req) 87 | 88 | return cacheRes || fetchRes 89 | } 90 | })()) 91 | } 92 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { Tracer } from 'opentracing' 2 | import { Namespace } from 'cls-hooked' 3 | import { ListenerFn } from 'eventemitter3' 4 | import { Connection } from 'amqplib' 5 | 6 | /** 7 | * Options used when making the initial Remit connection. 8 | */ 9 | interface RemitOptions { 10 | /** 11 | * The RabbitMQ exchange to use for this Remit instance. 12 | * 13 | * @default 'remit' 14 | */ 15 | exchange?: string 16 | 17 | /** 18 | * This service name will be used for tracing and RabbitMQ to help identify the connection. 19 | * 20 | * Defaults to the `REMIT_NAME` environment variable or 'remit' 21 | */ 22 | name?: string 23 | 24 | /** 25 | * The URL where RabbitMQ is located. 26 | * 27 | * Defaults to the `REMIT_URL` environment variable or 'amqp://localhost' 28 | */ 29 | url?: string 30 | 31 | /** 32 | * The tracer to be used for this Remit instance. 33 | * 34 | * The Jaeger tracer is excellent here. 35 | * 36 | * Defaults to a no-op stub tracer. 37 | */ 38 | tracer?: Tracer 39 | 40 | /** 41 | * The CLS context to be used with this Remit instance. 42 | * 43 | * Continuation Local Storage is a method of sharing context across asynchronous calls. 44 | * Remit greatly benefits from this for easier tracing for the end user. 45 | * 46 | * If you want to share the context with other tracers, provide the CLS namespace here. 47 | * 48 | * Internally, Remit sets the 'context' key. 49 | */ 50 | namespace?: Namespace 51 | 52 | /** 53 | * An existing AMQP connection to be used instead of making a new connection. 54 | */ 55 | connection?: Connection 56 | } 57 | 58 | /** 59 | * Remit constructor used to make new Remit instances. 60 | * 61 | * @returns {Remit.Remit} A fresh Remit instance. 62 | */ 63 | declare function Remit(options?: RemitOptions): Remit.Remit 64 | 65 | /** 66 | * A RMIE TEUIT 67 | */ 68 | declare namespace Remit { 69 | interface GlobalRequest { 70 | /** 71 | * Used to create a new Request to fetch data from an Endpoint. 72 | * These Requests can be re-used many times to request a response from the same Endpoint with differing data. 73 | * 74 | * @param event The event that this Request will target to receive data from or a set of options which must also contain `event`. 75 | */ 76 | (event: string | RequestOptions): Request 77 | on(event: 'sent' | 'error' | 'success' | 'data' | 'timeout', fn: ListenerFn, context?: any): this 78 | } 79 | 80 | interface GlobalEmitter { 81 | /** 82 | * Used to create a new Emitter to emit data to Listeners. 83 | * These Emitters can be re-used many times to emit data to Listeners with differing data. 84 | * 85 | * @param event The event that this Emitter will emit data to Listeners on or a set of options which must also contain `event`. 86 | */ 87 | (event: string | EmitterOptions): Emitter 88 | on(event: 'sent', fn: ListenerFn, context?: any): this 89 | } 90 | 91 | interface GlobalEndpoint { 92 | /** 93 | * Used to create a new Request to fetch data from an Endpoint. 94 | * These Requests can be re-used many times to request a response from the same Endpoint with differing data. 95 | * 96 | * Used to create a new Endpoint to listen to data from and respond to Requests. 97 | * An Endpoint must be created, a `.handler()` set, and then be `.start()`ed to receive requests. 98 | * 99 | * @param event The event that this Endpoint should respond to or a set of options which must also contain `event`. 100 | * @param handlers A function or set of functions used to respond to the event. Optional here, but a handler is required to `.start()` the Endpoint. 101 | */ 102 | (event: string | EndpointOptions, ...handlers: EndpointHandler[]): Endpoint 103 | on(event: 'data' | 'sent', fn: ListenerFn, context?: any): this 104 | } 105 | 106 | 107 | interface GlobalListener { 108 | /** 109 | * Used to create a new Listener to listen to data from Emitters. 110 | * A Listener must be created, a `.handler()` set, and then be `.start()`ed to receive emissions. 111 | * 112 | * @param event The event that this Listener should listen to data from or a set of options which must also contain `event`. 113 | * @param handlers A function or set of functions used to handle the incoming data. Optional here, but a handler is required to `.start()` the Listener. 114 | */ 115 | (event: string | ListenerOptions, ...handlers: ListenerHandler[]): Listener 116 | on(event: 'data', fn: ListenerFn, context?: any): this 117 | } 118 | 119 | /** 120 | * An instance of Remit. 121 | */ 122 | export interface Remit { 123 | /** 124 | * Used to create a new Request to fetch data from an Endpoint. 125 | * These Requests can be re-used many times to request a response from the same Endpoint with differing data. 126 | * 127 | * Also offers `.on()`, allowing you to add listeners to all Requests created on this Remit instance. 128 | */ 129 | request: GlobalRequest 130 | 131 | /** 132 | * Used to create a new Emitter to emit data to Listeners. 133 | * These Emitters can be re-used many times to emit data to Listeners with differing data. 134 | * 135 | * Also offers `.on()`, allowing you to add listeners to all Emitters created on this Remit instance. 136 | */ 137 | emit: GlobalEmitter 138 | 139 | /** 140 | * Used to create a new Endpoint to return data to Requests. 141 | * An Endpoint must be created, a `.handler()` set, and then be `.start()`ed to receive requests. 142 | * 143 | * Also offers `.on()`, allowing you to add listeners to all Endpoints created on this Remit instance. 144 | */ 145 | endpoint: GlobalEndpoint 146 | 147 | /** 148 | * Used to create a new Listener to listen to data from Emitters. 149 | * A Listener must be created, a `.handler()` set, and then be `.start()`ed to receive emissions. 150 | * 151 | * Also offers `.on()`, allowing you to add listeners to all Listeners created on this Remit instance. 152 | */ 153 | listen: GlobalListener 154 | 155 | /** 156 | * Allows you to add listeners to all components of this Remit instance. 157 | */ 158 | on(event: 'sent' | 'error' | 'success' | 'data' | 'timeout', fn: ListenerFn, context?: any): this 159 | 160 | /** 161 | * The version of the Remit package currently being used. 162 | */ 163 | version: string 164 | } 165 | 166 | /** 167 | * A parsed event from an AMQP message. 168 | */ 169 | export interface Event { 170 | /** The unique ID of the message. Also serves as RabbitMQ's internal correlation ID. */ 171 | eventId: string 172 | 173 | /** The routing key that the message used. */ 174 | eventType: string 175 | 176 | /** The `name` of the Remit instance that sent this message. */ 177 | resource: string 178 | 179 | /** The data contained within the event. */ 180 | data: any 181 | 182 | /** If the message is being received on an Endpoint or a Listener, this will be the time at which the message was served to a handler after being pulled from the server and parsed. */ 183 | started?: Date 184 | 185 | /** If the message was scheduled for a specific time, this is the Date for which it was scheduled. */ 186 | scheduled?: Date 187 | 188 | /** If the message was intended to be delayed, this is the amount of time (in milliseconds) that it was intended to be delayed for. */ 189 | delay?: number 190 | 191 | /** The file and line number that the message was triggered from. This could be a file at another process. */ 192 | resourceTrace?: string 193 | 194 | /** The Date at which the message was originally sent. */ 195 | timestamp?: Date 196 | } 197 | 198 | export interface RequestOptions { 199 | /** The amount of time after which a Request will give up and throw an error. Can be either an integer representing milliseconds or an `ms`-compatible string like '5s' or '1m'. */ 200 | timeout?: string | number 201 | /** The event that this Request will target to receive data from. */ 202 | event?: string 203 | /** The priority of the message from 0 to 10. Higher priority messages will be taken off the queue before lower priority ones. A higher number denotes a higher priority. */ 204 | priority?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 205 | } 206 | 207 | export interface ListenerOptions { 208 | /** The event that this Listener should listen to data from. */ 209 | event?: string 210 | 211 | /** The queue name in RabbitMQ is generated based on the `event` provided. You can use this to customise the queue name created in RabbitMQ to this string. */ 212 | queue?: string 213 | 214 | /** 215 | * The maximum number of unhandled messages the Listener will pull from RabbitMQ. 216 | * 217 | * @default 48 218 | */ 219 | prefetch?: number 220 | 221 | /** If this is true, an entirely unique, exclusive queue will be generated to consume from, but it will also be deleted upon the listener closing. This is good for creating pubsub-style listeners with no persistence. */ 222 | subscribe?: boolean 223 | } 224 | 225 | export interface EmitterOptions { 226 | /** The event that this Emitter will emit data to Listeners on */ 227 | event?: string 228 | 229 | /** The delay after which or the Date at which the message should be available to listeners. The message will be held on RabbitMQ until it's ready. Can be either an integer representing or an `ms`-compatible string like '5s' or '1m' for a delay, or a Date to schedule for a particular time. */ 230 | delay?: string | Date | number 231 | 232 | /** The priority of the message from 0 to 10. Higher priority messages will be taken off the queue before lower priority ones. A higher number denotes a higher priority. */ 233 | priority?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 234 | } 235 | 236 | export interface EndpointOptions { 237 | /** The event that this Endpoint should respond to */ 238 | event?: string 239 | 240 | /** The queue name in RabbitMQ is generated based on the `event` provided. You can use this to customise the queue name created in RabbitMQ to this string. */ 241 | queue?: string 242 | 243 | /** 244 | * The maximum number of unhandled messages the Listener will pull from RabbitMQ. 245 | * 246 | * @default 48 247 | */ 248 | prefetch?: number 249 | } 250 | 251 | export type Handler = (event: Remit.Event) => any 252 | export type EndpointHandler = Handler 253 | export type ListenerHandler = Handler 254 | 255 | export interface Request { 256 | /** 257 | * Sends the request with the given data. Running the Request itself and running the `.send()` method are the same thing. 258 | * 259 | * @param data The data you wish to send. Anything compatibile with JSON.stringify() works here, or a warning will be logged and the data set to NULL. Objects are good here. 260 | * @param opts Any options passed in here will take effect for this singular Request only. Good for setting timeouts in very particular circumstances. 261 | * 262 | * @returns {Promise} Returns a promise that resolves with the data returned from the Endpoint. 263 | */ 264 | (data?: any, opts?: Remit.RequestOptions): Promise 265 | 266 | /** 267 | * Change the options of this Request instance. 268 | * 269 | * @param options An object of options for the Request. 270 | * 271 | * @returns {Request} The instance of Request for chaining purposes. 272 | */ 273 | options(options: Remit.RequestOptions): this 274 | 275 | /** 276 | * Set fallback data for if a request fails. 277 | * If fallback data is set, a request can never reject, but will instead resolve with the fallback data. 278 | * The fallback can be unset by running the function with no parameters. 279 | * 280 | * If using fallback data, it's a good idea to add an 'error' listener via `.on('error', ...)` to ensure errors aren't lost in the mix. 281 | * 282 | * @param data Some fallback data. 283 | * 284 | * @returns {Request} The instance of Request for chaining purposes. 285 | */ 286 | fallback(data?: any): this 287 | 288 | /** 289 | * Return a promise for when the Request is ready to send messages. 290 | * This isn't a requirement to watch as any requests sent before we're ready are queued up, but it's sometimes useful to see. 291 | * 292 | * @returns {Promise} A promise which resolves with the instance of Request once the Request is ready. 293 | */ 294 | ready(): Promise 295 | 296 | /** 297 | * Sends the request with the given data. Running the Request itself and running the `.send()` method are the same thing. 298 | * 299 | * @param data The data you wish to send. Anything compatibile with JSON.stringify() works here, or a warning will be logged and the data set to NULL. Objects are good here. 300 | * @param opts Any options passed in here will take effect for this singular Request only. Good for setting timeouts in very particular circumstances. 301 | * 302 | * @returns {Promise} Returns a promise that resolves with the data returned from the Endpoint. 303 | */ 304 | send: (data?: any, opts?: Remit.RequestOptions) => Promise 305 | 306 | /** 307 | * Add a listener to the various internal events of this Request instance. 308 | * 309 | * @param event The event to listen to. 310 | * @param fn The callback to run when the event happens. 311 | * 312 | * @returns {Request} The instance of Request for chaining purposes. 313 | */ 314 | on(event: 'sent' | 'error' | 'success' | 'data' | 'timeout', fn: ListenerFn, context?: any): this 315 | } 316 | 317 | export interface Emitter { 318 | /** 319 | * Emits the given data. Running the Emitter itself and running the `.send()` method are the same thing. 320 | * 321 | * @param data The data you wish to send. Anything compatibile with JSON.stringify() works here, or a warning will be logged and the data set to NULL. Objects are good here. 322 | * @param opts Any options passed in here will take effect for this singular Emitter only. 323 | * 324 | * @returns {Promise} Returns a promise that resolves with the internal event sent to Listeners once the message has been sent. 325 | */ 326 | (data?: any, opts?: Remit.EmitterOptions): Promise 327 | 328 | /** 329 | * Change the options of this Emitter instance. 330 | * 331 | * @param options An object of options for the Emitter. 332 | * 333 | * @returns {Emitter} The instance of Emitter for chaining purposes. 334 | */ 335 | options(options: Remit.EmitterOptions): this 336 | 337 | /** 338 | * Return a promise for when the Emitter is ready to send messages. 339 | * This isn't a requirement to watch as any emissions sent before we're ready are queued up, but it's sometimes useful to see. 340 | * 341 | * @returns {Promise} A promise which resolves with the instance of Emitter once it's ready to send messages. 342 | */ 343 | ready(): Promise 344 | 345 | /** 346 | * Emits the given data. Running the Emitter itself and running the `.send()` method are the same thing. 347 | * 348 | * @param data The data you wish to send. Anything compatibile with JSON.stringify() works here, or a warning will be logged and the data set to NULL. Objects are good here. 349 | * @param opts Any options passed in here will take effect for this singular Emitter only. 350 | * 351 | * @returns {Promise} Returns a promise that resolves with the internal event sent to Listeners. 352 | */ 353 | send: (data?: any, opts?: Remit.EmitterOptions) => Promise 354 | 355 | /** 356 | * Add a listener to the various internal events of this Emitter instance. 357 | * 358 | * @param event The event to listen to. 359 | * @param fn The callback to run when the event happens. 360 | * 361 | * @returns {Emitter} The instance of Emitter for chaining purposes. 362 | */ 363 | on(event: 'sent', fn: ListenerFn, context?: any): this 364 | } 365 | 366 | export interface Endpoint { 367 | /** 368 | * Change the options of this Endpoint instance. 369 | * 370 | * @param options An object of options for the Endpoint. 371 | * 372 | * @returns {Endpoint} The instance of Endpoint for chaining purposes. 373 | */ 374 | options(options: EndpointOptions): this 375 | 376 | /** 377 | * Set the handlers for this Endpoint. If multiple handlers are given, the functions are treated as a chain which the data will be passed through. See the docs for more information on how this is handled. 378 | * 379 | * @param handlers A set of functions used to handle incoming data from Requests. 380 | * 381 | * @returns {Endpoint} The instance of Endpoint for chaining purposes. 382 | */ 383 | handler(...handlers: EndpointHandler[]): this 384 | 385 | /** 386 | * Start consuming and processing messages from RabbitMQ. 387 | * 388 | * @returns {Promise} The instance of Endpoint for chaining purposes once the Endpoint is started. 389 | */ 390 | start(): Promise 391 | 392 | /** 393 | * Resume consumption of messages after being paused. 394 | * 395 | * Starts the Endpoint if not already started. Has no effect if already running. 396 | * 397 | * @returns {Promise} A promise that resolves with the Endpoint once the Endpoint has successfully resumed. 398 | */ 399 | resume(): Promise 400 | 401 | /** 402 | * Pause consumption of messages. Has no effect if the Endpoint is not currently started. 403 | * 404 | * @param cold If true, any messages currently being processed will be cancelled and passed back to RabbitMQ to be picked up by another instance. Otherwise no new messages will arrive, but currently-held ones will be processed. 405 | * 406 | * @returns {Promise} A promise that resolves with the Endpoint once consumption has been successfully paused. This does not include any lingering messages if it's a warm pause. 407 | */ 408 | pause(cold: boolean): Promise 409 | 410 | /** 411 | * Add a listener to the various internal events of this Endpoint instance. 412 | * 413 | * @param event The event to listen to. 414 | * @param fn The callback to run when the event happens. 415 | * 416 | * @returns {Endpoint} The instance of Endpoint for chaining purposes. 417 | */ 418 | on(event: 'data' | 'sent', fn: ListenerFn, context?: any): this 419 | } 420 | 421 | export interface Listener { 422 | /** 423 | * Change the options of this Listener instance. 424 | * 425 | * @param options An object of options for the Listener. 426 | * 427 | * @returns {Listener} The instance of Listener for chaining purposes. 428 | */ 429 | options(options: ListenerOptions): this 430 | 431 | /** 432 | * Set the handlers for this Listener. If multiple handlers are given, the functions are treated as a chain which the data will be passed through. See the docs for more information on how this is handled. 433 | * 434 | * @param handlers A set of functions used to handle incoming data from Emitters. 435 | * 436 | * @returns {Listener} The instance of Listener for chaining purposes. 437 | */ 438 | handler(...handlers: ListenerHandler[]): this 439 | 440 | /** 441 | * Start consuming and processing messages from RabbitMQ. 442 | * 443 | * @returns {Promise} The instance of Listener for chaining purposes once the Listener is started. 444 | */ 445 | start(): Promise 446 | 447 | /** 448 | * Resume consumption of messages after being paused. 449 | * 450 | * Starts the Listener if not already started. Has no effect if already running. 451 | * 452 | * @returns {Promise} A promise that resolves with the Listener once the Listener has successfully resumed. 453 | */ 454 | resume(): Promise 455 | 456 | /** 457 | * Pause consumption of messages. Has no effect if the Listener is not currently started. 458 | * 459 | * @param cold If true, any messages currently being processed will be cancelled and passed back to RabbitMQ to be picked up by another instance. Otherwise no new messages will arrive, but currently-held ones will be processed. 460 | * 461 | * @returns {Promise} A promise that resolves with the Listener once consumption has been successfully paused. This does not include any lingering messages if it's a warm pause. 462 | */ 463 | pause(cold: boolean): Promise 464 | 465 | /** 466 | * Add a listener to the various internal events of this Listener instance. 467 | * 468 | * @param event The event to listen to. 469 | * @param fn The callback to run when the event happens. 470 | * 471 | * @returns {Listener} The instance of Listener for chaining purposes. 472 | */ 473 | on(event: 'data', fn: ListenerFn, context?: any): this 474 | } 475 | } 476 | 477 | export = Remit 478 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Remit = require('./lib/Remit') 2 | 3 | function remit (options) { 4 | return new Remit(options) 5 | } 6 | 7 | module.exports = remit 8 | -------------------------------------------------------------------------------- /lib/Emitter.js: -------------------------------------------------------------------------------- 1 | const opentracing = require('opentracing') 2 | const CallableInstance = require('callable-instance') 3 | const EventEmitter = require('eventemitter3') 4 | const genUuid = require('../utils/genUuid') 5 | const parseEvent = require('../utils/parseEvent') 6 | const getStackLine = require('../utils/getStackLine') 7 | const ms = require('ms') 8 | 9 | class Emitter extends CallableInstance { 10 | constructor (remit, opts = {}) { 11 | super('send') 12 | 13 | this._remit = remit 14 | this._emitter = new EventEmitter() 15 | 16 | let parsedOpts = {} 17 | 18 | if (typeof opts === 'string') { 19 | parsedOpts.event = opts 20 | } else { 21 | parsedOpts = opts 22 | } 23 | 24 | if (!parsedOpts.event) { 25 | throw new Error('No/invalid event specified when creating an emission') 26 | } 27 | 28 | this.options(parsedOpts) 29 | 30 | this._ready = Promise.resolve(this) 31 | } 32 | 33 | on (...args) { 34 | // should we warn/block users when they try 35 | // to listen to an event that doesn't exist? 36 | this._emitter.on(...args) 37 | 38 | return this 39 | } 40 | 41 | options (opts = {}) { 42 | this._options = this._generateOptions(opts) 43 | 44 | return this 45 | } 46 | 47 | ready () { 48 | return this._ready 49 | } 50 | 51 | send (...args) { 52 | while (args.length < 2) { 53 | args.push(undefined) 54 | } 55 | 56 | if (!this._remit._namespace.active) args[2] = true 57 | 58 | return this._remit._namespace.active 59 | ? this._send(...args) 60 | : this._remit._namespace.runAndReturn(this._send.bind(this, ...args)) 61 | } 62 | 63 | async _send (data = null, opts = {}, extendedCapture = false) { 64 | // parse the callsites here, as after the `await` 65 | // we'll get a different stack 66 | const callsites = getStackLine.capture() 67 | const now = new Date().getTime() 68 | const parsedOptions = this._generateOptions(opts) 69 | const messageId = genUuid() 70 | 71 | const message = { 72 | mandatory: false, 73 | messageId: messageId, 74 | appId: this._remit._options.name, 75 | timestamp: now, 76 | headers: { 77 | trace: getStackLine.parse(callsites) 78 | }, 79 | persistent: true 80 | } 81 | 82 | if (parsedOptions.priority) { 83 | if (parsedOptions.priority > 10 || parsedOptions.priority < 0) { 84 | throw new Error(`Invalid priority "${parsedOptions.priority}" when making request`) 85 | } 86 | 87 | message.priority = parsedOptions.priority 88 | } 89 | 90 | let parsedData 91 | 92 | // coerce data to `null` if undefined or an unparsable pure JS property. 93 | parsedData = JSON.stringify(data) 94 | 95 | if (typeof parsedData === 'undefined') { 96 | console.warn('[WARN] Remit emit sent with unparsable JSON; this could be a function or an undefined variable. Data instead set to NULL.') 97 | 98 | // string here coerces to actual NULL once JSON.parse is performed 99 | parsedData = 'null' 100 | } 101 | 102 | const parentContext = this._remit._namespace.get('context') 103 | 104 | const span = this._remit._tracer.startSpan(`Remit Emit: ${parsedOptions.event}`, { 105 | tags: { 106 | 'remit.version': this._remit.version, 107 | [opentracing.Tags.SAMPLING_PRIORITY]: 1, 108 | [opentracing.Tags.COMPONENT]: 'remit', 109 | [opentracing.Tags.MESSAGE_BUS_DESTINATION]: parsedOptions.event, 110 | [opentracing.Tags.SPAN_KIND]: opentracing.Tags.SPAN_KIND_MESSAGING_PRODUCER, 111 | 'data.outgoing': data 112 | }, 113 | childOf: parentContext 114 | }) 115 | 116 | this._remit._tracer.inject(span.context(), opentracing.FORMAT_TEXT_MAP, message.headers.context) 117 | 118 | const demitQueue = await this._setupDemitQueue(parsedOptions, now) 119 | const worker = await this._remit._workers.acquire() 120 | 121 | try { 122 | if (demitQueue) { 123 | const { queue, expiration } = demitQueue 124 | 125 | if (parsedOptions.schedule) { 126 | message.headers.scheduled = +parsedOptions.schedule 127 | message.expiration = expiration 128 | } else { 129 | message.headers.delay = parsedOptions.delay 130 | } 131 | 132 | worker.sendToQueue( 133 | queue, 134 | Buffer.from(parsedData), 135 | message 136 | ) 137 | } else { 138 | worker.publish( 139 | this._remit._exchange, 140 | parsedOptions.event, 141 | Buffer.from(parsedData), 142 | message 143 | ) 144 | } 145 | 146 | this._remit._workers.release(worker) 147 | span.finish() 148 | 149 | // We do this to make room for multiple emits. 150 | // without this, continued synchronous emissions 151 | // never get a chance to send 152 | await new Promise(resolve => setImmediate(resolve)) 153 | 154 | const event = parseEvent(message, { 155 | routingKey: parsedOptions.event 156 | }, JSON.parse(parsedData), { 157 | flowType: 'exit' 158 | }) 159 | 160 | this._emitter.emit('sent', event) 161 | 162 | return event 163 | } catch (e) { 164 | this._remit._workers.destroy(worker) 165 | throw e 166 | } 167 | } 168 | 169 | _generateOptions (opts = {}) { 170 | const parsedOpts = {} 171 | 172 | if (opts.hasOwnProperty('delay')) { 173 | if (typeof opts.delay === 'string') { 174 | parsedOpts.delay = ms(opts.delay) 175 | parsedOpts.schedule = null 176 | } else if (opts.delay instanceof Date && !isNaN(opts.delay)) { 177 | parsedOpts.delay = null 178 | parsedOpts.schedule = opts.delay 179 | } else { 180 | parsedOpts.delay = opts.delay 181 | parsedOpts.schedule = null 182 | } 183 | } 184 | 185 | return Object.assign({}, this._options || {}, opts, parsedOpts) 186 | } 187 | 188 | async _setupDemitQueue (opts, time) { 189 | if (isNaN(opts.delay) && !opts.schedule) { 190 | return false 191 | } 192 | 193 | if ( 194 | (!opts.delay || isNaN(opts.delay)) && 195 | (!opts.schedule || !(opts.schedule instanceof Date) || opts.schedule.toString() === 'Invalid Date') 196 | ) { 197 | throw new Error('Invalid delay date or duration when attempting to send a delayed emission') 198 | } 199 | 200 | const group = opts.schedule ? +opts.schedule : opts.delay 201 | const expiration = opts.schedule ? (+opts.schedule - time) : opts.delay 202 | 203 | if (expiration < 1) { 204 | return false 205 | } 206 | 207 | const queueOpts = { 208 | exclusive: false, 209 | durable: true, 210 | autoDelete: true, 211 | deadLetterExchange: this._remit._exchange, 212 | deadLetterRoutingKey: opts.event 213 | } 214 | 215 | if (opts.delay) { 216 | queueOpts.messageTtl = expiration 217 | queueOpts.expires = expiration * 2 218 | } else { 219 | queueOpts.expires = expiration + 60000 220 | } 221 | 222 | const worker = await this._remit._workers.acquire() 223 | const queue = `d:${this._remit._exchange}:${opts.event}:${group}` 224 | 225 | try { 226 | await worker.assertQueue(queue, queueOpts) 227 | this._remit._workers.release(worker) 228 | return { queue, expiration } 229 | } catch (e) { 230 | this._remit._workers.destroy(worker) 231 | 232 | // if we're scheduling an emission and we have an inequivalent 233 | // x-expires argument, that's fine; that'll happen 234 | if (opts.schedule && e.message && e.message.substr(94, 28) === 'inequivalent arg \'x-expires\'') { 235 | return { queue, expiration } 236 | } else { 237 | throw e 238 | } 239 | } 240 | } 241 | } 242 | 243 | module.exports = Emitter 244 | -------------------------------------------------------------------------------- /lib/Endpoint.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('eventemitter3') 2 | const opentracing = require('opentracing') 3 | const parseEvent = require('../utils/parseEvent') 4 | const waterfall = require('../utils/asyncWaterfall') 5 | const serializeData = require('../utils/serializeData') 6 | const handlerWrapper = require('../utils/handlerWrapper') 7 | const throwAsException = require('../utils/throwAsException') 8 | 9 | class Endpoint { 10 | constructor (remit, opts, ...handlers) { 11 | this._remit = remit 12 | this._emitter = new EventEmitter() 13 | 14 | let parsedOpts = {} 15 | 16 | if (typeof opts === 'string') { 17 | parsedOpts.event = opts 18 | } else { 19 | parsedOpts = opts || {} 20 | } 21 | 22 | if (!parsedOpts.event) { 23 | throw new Error('No/invalid event specified when creating an endpoint') 24 | } 25 | 26 | this.options(parsedOpts) 27 | 28 | if (handlers.length) { 29 | this.handler(...handlers) 30 | } 31 | } 32 | 33 | handler (...fns) { 34 | if (!fns.length) { 35 | throw new Error('No handler(s) given when trying to set endpoint handler(s)') 36 | } 37 | 38 | this._handler = waterfall(...fns.map(handlerWrapper)) 39 | 40 | return this 41 | } 42 | 43 | on (...args) { 44 | // should we warn/block users when they try 45 | // to listen to an event that doesn't exist? 46 | this._emitter.on(...args) 47 | 48 | return this 49 | } 50 | 51 | options (opts = {}) { 52 | opts.queue = opts.queue || opts.event || this._options.queue || this._options.event 53 | this._options = Object.assign({}, this._options || {}, opts) 54 | 55 | return this 56 | } 57 | 58 | // TODO should we emit something once booted? 59 | start () { 60 | if (this._started) { 61 | return this._started 62 | } 63 | 64 | if (!this._handler) { 65 | throw new Error('Trying to boot endpoint with no handler') 66 | } 67 | 68 | this._started = this._setup(this._options) 69 | 70 | return this._started 71 | } 72 | 73 | pause (cold) { 74 | if (!this._started) { 75 | return Promise.resolve(this) 76 | } 77 | 78 | if (this._paused) { 79 | if (this._resuming) { 80 | console.warn('Tried to pause endpoint whilst busy resuming') 81 | } 82 | 83 | return this._paused 84 | } 85 | 86 | this._paused = new Promise((resolve, reject) => { 87 | const ops = [this._consumer.cancel(this._consumerTag)] 88 | 89 | if (cold) { 90 | // cold pause requsted, so let's push all messages 91 | // back in to the queue rather than handling them 92 | this._cold = true 93 | ops.push(this._consumer.recover()) 94 | } 95 | 96 | return Promise.all(ops) 97 | .then(() => resolve(this)) 98 | .catch(reject) 99 | }) 100 | 101 | return this._paused 102 | } 103 | 104 | resume () { 105 | if (this._resuming) return this._resuming 106 | if (!this._started) return this.start() 107 | if (!this._starting && !this._paused) return Promise.resolve(this) 108 | 109 | this._resuming = new Promise(async (resolve, reject) => { 110 | let consumeResult 111 | 112 | try { 113 | consumeResult = await this._consumer.consume( 114 | this._options.queue, 115 | this._remit._namespace.bind(this._incoming.bind(this)), 116 | { 117 | noAck: true, 118 | exclusive: false 119 | } 120 | ) 121 | } catch (e) { 122 | delete this._resuming 123 | 124 | return reject(e) 125 | } 126 | 127 | this._consumerTag = consumeResult.consumerTag 128 | delete this._resuming 129 | delete this._paused 130 | delete this._cold 131 | 132 | return resolve(this) 133 | }) 134 | 135 | return this._resuming 136 | } 137 | 138 | async _incoming (message) { 139 | if (!message) { 140 | await throwAsException(new Error('Consumer cancelled unexpectedly; this was most probably done via RabbitMQ\'s management panel')) 141 | } 142 | 143 | try { 144 | var data = JSON.parse(message.content.toString()) 145 | } catch (e) { 146 | // if this fails, there's no need to nack, 147 | // so just ignore it 148 | return 149 | } 150 | 151 | const parentContext = this._remit._tracer.extract(opentracing.FORMAT_TEXT_MAP, (message.properties.headers && message.properties.headers.context) || {}) || null 152 | 153 | const span = this._remit._tracer.startSpan(`Remit Endpoint: ${this._options.event}`, { 154 | tags: { 155 | 'remit.version': this._remit.version, 156 | [opentracing.Tags.SAMPLING_PRIORITY]: 1, 157 | [opentracing.Tags.COMPONENT]: 'remit', 158 | [opentracing.Tags.MESSAGE_BUS_DESTINATION]: this._options.event, 159 | [opentracing.Tags.SPAN_KIND]: opentracing.Tags.SPAN_KIND_RPC_SERVER, 160 | 'data.incoming': data 161 | }, 162 | childOf: parentContext 163 | }) 164 | 165 | this._remit._namespace.set('context', span.context()) 166 | 167 | const event = parseEvent(message.properties, message.fields, data, { 168 | isReceiver: true 169 | }) 170 | 171 | const resultOp = this._handler(event) 172 | 173 | try { 174 | this._emitter.emit('data', event) 175 | } catch (e) { 176 | console.error(e) 177 | } 178 | 179 | const canReply = Boolean(message.properties.replyTo) 180 | 181 | let finalData = await resultOp 182 | const [ resErr, resData ] = finalData 183 | 184 | if (resErr) { 185 | span.setTag(opentracing.Tags.ERROR, true) 186 | span.setTag('data.outgoing', resErr) 187 | } else { 188 | span.setTag('data.outgoing', resData) 189 | } 190 | 191 | span.finish() 192 | 193 | // if a cold pause has been requested, don't process this 194 | if (this._cold) return 195 | 196 | if (canReply) { 197 | finalData = serializeData(finalData) 198 | 199 | const worker = await this 200 | ._remit 201 | ._workers 202 | .acquire() 203 | 204 | try { 205 | await worker.sendToQueue( 206 | message.properties.replyTo, 207 | Buffer.from(finalData), 208 | message.properties 209 | ) 210 | 211 | this._remit._workers.release(worker) 212 | 213 | const event = parseEvent(message.properties, { 214 | routingKey: this._options.event 215 | }, finalData) 216 | 217 | this._emitter.emit('sent', event) 218 | } catch (e) { 219 | this._remit._workers.destroy(worker) 220 | } 221 | } 222 | } 223 | 224 | async _setup ({ queue, event, prefetch = 48 }) { 225 | this._starting = true 226 | 227 | try { 228 | const worker = await this._remit._workers.acquire() 229 | 230 | try { 231 | await worker.assertQueue(queue, { 232 | exclusive: false, 233 | durable: true, 234 | autoDelete: false, 235 | maxPriority: 10 236 | }) 237 | 238 | this._remit._workers.release(worker) 239 | } catch (e) { 240 | delete this._starting 241 | this._remit._workers.destroy(worker) 242 | throw e 243 | } 244 | 245 | const connection = await this._remit._connection 246 | this._consumer = await connection.createChannel() 247 | this._consumer.on('error', console.error) 248 | this._consumer.on('close', () => { 249 | throwAsException(new Error('Consumer died - this is most likely due to the RabbitMQ connection dying')) 250 | }) 251 | 252 | if (prefetch > 0) { 253 | this._consumer.prefetch(prefetch, true) 254 | } 255 | 256 | await this._consumer.bindQueue( 257 | queue, 258 | this._remit._exchange, 259 | event 260 | ) 261 | 262 | await this.resume() 263 | delete this._starting 264 | 265 | return this 266 | } catch (e) { 267 | delete this._starting 268 | await throwAsException(e) 269 | } 270 | } 271 | } 272 | 273 | module.exports = Endpoint 274 | -------------------------------------------------------------------------------- /lib/Listener.js: -------------------------------------------------------------------------------- 1 | const opentracing = require('opentracing') 2 | const EventEmitter = require('eventemitter3') 3 | const parseEvent = require('../utils/parseEvent') 4 | const waterfall = require('../utils/asyncWaterfall') 5 | const handlerWrapper = require('../utils/handlerWrapper') 6 | const throwAsException = require('../utils/throwAsException') 7 | 8 | class Listener { 9 | constructor (remit, opts, ...handlers) { 10 | this._remit = remit 11 | this._emitter = new EventEmitter() 12 | 13 | let parsedOpts = {} 14 | 15 | if (typeof opts === 'string') { 16 | parsedOpts.event = opts 17 | } else { 18 | parsedOpts = opts 19 | } 20 | 21 | if (!parsedOpts.event) { 22 | throw new Error('No/invalid event specified when creating an endpoint') 23 | } 24 | 25 | this.options(parsedOpts) 26 | 27 | if (handlers.length) { 28 | this.handler(...handlers) 29 | } 30 | } 31 | 32 | handler (...fns) { 33 | this._handler = waterfall(...fns.map(handlerWrapper)) 34 | 35 | return this 36 | } 37 | 38 | on (...args) { 39 | // should we warn/block users when they try 40 | // to listen to an event that doesn't exist? 41 | this._emitter.on(...args) 42 | 43 | return this 44 | } 45 | 46 | options (opts = {}) { 47 | const event = opts.event || this._options.event 48 | this._remit._eventCounters[event] = this._remit._eventCounters[event] || 0 49 | 50 | opts.queue = opts.queue || `${opts.event || this._options.event}:l:${this._remit._options.name}:${++this._remit._eventCounters[event]}` 51 | 52 | this._options = Object.assign({}, this._options || {}, opts) 53 | 54 | return this 55 | } 56 | 57 | start () { 58 | if (this._started) { 59 | return this._started 60 | } 61 | 62 | if (!this._handler) { 63 | throw new Error('Trying to boot listener with no handler') 64 | } 65 | 66 | this._started = this._setup(this._options) 67 | 68 | return this._started 69 | } 70 | 71 | pause (cold) { 72 | if (!this._started) { 73 | return Promise.resolve(this) 74 | } 75 | 76 | if (this._paused) { 77 | if (this._resuming) { 78 | console.warn('Tried to pause listener whilst busy resuming') 79 | } 80 | 81 | return this._paused 82 | } 83 | 84 | this._paused = new Promise((resolve, reject) => { 85 | const ops = [this._consumer.cancel(this._consumerTag)] 86 | 87 | if (cold) { 88 | // cold pause requested, so let's push all messages 89 | // back in to the queue rather than handling them 90 | this._cold = true 91 | ops.push(this._consumer.recover()) 92 | } 93 | 94 | return Promise.all(ops) 95 | .then(() => resolve(this)) 96 | .catch(reject) 97 | }) 98 | 99 | return this._paused 100 | } 101 | 102 | resume () { 103 | if (this._resuming) return this._resuming 104 | if (!this._started) return this.start() 105 | if (!this._starting && !this._paused) return Promise.resolve(this) 106 | 107 | this._resuming = new Promise(async (resolve, reject) => { 108 | const shouldSubscribe = Boolean(this._options.subscribe) 109 | let consumeResult 110 | 111 | try { 112 | consumeResult = await this._consumer.consume( 113 | this._consumerQueueName, 114 | this._remit._namespace.bind(this._incoming.bind(this)), 115 | { 116 | noAck: shouldSubscribe, 117 | exclusive: shouldSubscribe 118 | } 119 | ) 120 | } catch (e) { 121 | delete this._resuming 122 | 123 | return reject(e) 124 | } 125 | 126 | this._consumerTag = consumeResult.consumerTag 127 | delete this._resuming 128 | delete this._paused 129 | delete this._cold 130 | 131 | return resolve(this) 132 | }) 133 | 134 | return this._resuming 135 | } 136 | 137 | async _incoming (message) { 138 | if (!message) { 139 | await throwAsException(new Error('Consumer cancelled unexpectedly; this was most probably done via RabbitMQ\'s management panel')) 140 | } 141 | 142 | try { 143 | var data = JSON.parse(message.content.toString()) 144 | } catch (e) { 145 | // if this fails, let's just nack the message and leave 146 | this._consumer.nack(message) 147 | 148 | return 149 | } 150 | 151 | const parentContext = this._remit._tracer.extract(opentracing.FORMAT_TEXT_MAP, (message.properties.headers && message.properties.headers.context) || {}) || null 152 | 153 | const span = this._remit._tracer.startSpan(`Remit Listener: ${this._options.event}`, { 154 | tags: { 155 | 'remit.version': this._remit.version, 156 | [opentracing.Tags.SAMPLING_PRIORITY]: 1, 157 | [opentracing.Tags.COMPONENT]: 'remit', 158 | [opentracing.Tags.MESSAGE_BUS_DESTINATION]: this._options.event, 159 | [opentracing.Tags.SPAN_KIND]: opentracing.Tags.SPAN_KIND_MESSAGING_CONSUMER, 160 | 'data.incoming': data 161 | }, 162 | references: [opentracing.followsFrom(parentContext)] 163 | }) 164 | 165 | this._remit._namespace.set('context', span.context()) 166 | 167 | const event = parseEvent(message.properties, message.fields, data, { 168 | isReceiver: true 169 | }) 170 | 171 | const resultOp = this._handler(event) 172 | 173 | try { 174 | this._emitter.emit('data', event) 175 | } catch (e) { 176 | console.error(e) 177 | } 178 | 179 | await resultOp 180 | span.finish() 181 | 182 | // if a cold pause has been requested, don't process this 183 | if (this._cold) return 184 | 185 | this._consumer.ack(message) 186 | } 187 | 188 | async _setup ({ queue, event, prefetch = 48, subscribe }) { 189 | this._starting = true 190 | const shouldSubscribe = Boolean(subscribe) 191 | 192 | try { 193 | const worker = await this._remit._workers.acquire() 194 | let ok 195 | 196 | try { 197 | ok = await worker.assertQueue(shouldSubscribe ? '' : queue, { 198 | exclusive: shouldSubscribe, 199 | durable: !shouldSubscribe, 200 | autoDelete: shouldSubscribe, 201 | maxPriority: 10 202 | }) 203 | 204 | this._remit._workers.release(worker) 205 | } catch (e) { 206 | delete this._starting 207 | this._remit._workers.destroy(worker) 208 | throw e 209 | } 210 | 211 | this._consumerQueueName = ok.queue 212 | const connection = await this._remit._connection 213 | this._consumer = await connection.createChannel() 214 | this._consumer.on('error', console.error) 215 | this._consumer.on('close', () => { 216 | throwAsException(new Error('Consumer died - this is most likely due to the RabbitMQ connection dying')) 217 | }) 218 | 219 | if (prefetch > 0) { 220 | this._consumer.prefetch(prefetch, true) 221 | } 222 | 223 | await this._consumer.bindQueue( 224 | this._consumerQueueName, 225 | this._remit._exchange, 226 | event 227 | ) 228 | 229 | await this.resume() 230 | delete this._starting 231 | 232 | return this 233 | } catch (e) { 234 | delete this._starting 235 | await throwAsException(e) 236 | } 237 | } 238 | } 239 | 240 | module.exports = Listener 241 | -------------------------------------------------------------------------------- /lib/Remit.js: -------------------------------------------------------------------------------- 1 | const url = require('url') 2 | const amqplib = require('amqplib') 3 | const { Tracer } = require('opentracing') 4 | const EventEmitter = require('eventemitter3') 5 | const packageJson = require('../package.json') 6 | const parseAmqpUrl = require('../utils/parseAmqpUrl') 7 | const generateConnectionOptions = require('../utils/generateConnectionOptions') 8 | const ChannelPool = require('../utils/ChannelPool') 9 | const CallableWrapper = require('../utils/CallableWrapper') 10 | const throwAsException = require('../utils/throwAsException') 11 | const Endpoint = require('./Endpoint') 12 | const Listener = require('./Listener') 13 | const Request = require('./Request') 14 | const Emitter = require('./Emitter') 15 | const { createNamespace } = require('cls-hooked') 16 | 17 | class Remit { 18 | constructor (options = {}) { 19 | this.listen = new CallableWrapper(this, Listener) 20 | this.emit = new CallableWrapper(this, Emitter) 21 | this.endpoint = new CallableWrapper(this, Endpoint) 22 | this.request = new CallableWrapper(this, Request) 23 | 24 | this.version = packageJson.version 25 | 26 | this._options = {} 27 | 28 | this._options.exchange = options.exchange || 'remit' 29 | this._options.name = options.name || process.env.REMIT_NAME || 'remit' 30 | this._options.url = options.url || process.env.REMIT_URL || 'amqp://localhost' 31 | 32 | this._emitter = new EventEmitter() 33 | this._connection = (options.connection 34 | ? this._ensureExchange(options.connection, this._options.exchange) 35 | : this._connect(this._options) 36 | ).catch(throwAsException) 37 | this._workers = ChannelPool(this._connection) 38 | this._publishChannels = {} 39 | 40 | this._tracer = options.tracer || new Tracer({ 41 | serviceName: this._options.name, 42 | reporter: { 43 | logSpans: true, 44 | flushIntervalMs: 10 45 | } 46 | }) 47 | 48 | this._namespace = options.namespace || createNamespace('remit') 49 | 50 | // TODO make this better 51 | this._eventCounters = {} 52 | } 53 | 54 | on (...args) { 55 | this._emitter.on(...args) 56 | } 57 | 58 | async _connect ({ url: unparsedUrl, name, exchange }) { 59 | const amqpUrl = parseAmqpUrl(unparsedUrl) 60 | const connectionOptions = generateConnectionOptions(name) 61 | const { hostname } = url.parse(amqpUrl) 62 | connectionOptions.servername = hostname 63 | const connection = await amqplib.connect(amqpUrl, connectionOptions) 64 | 65 | return this._ensureExchange(connection, exchange) 66 | } 67 | 68 | async _ensureExchange (connection, exchange) { 69 | const tempChannel = await connection.createChannel() 70 | 71 | await tempChannel.assertExchange(exchange, 'topic', { 72 | durable: true, 73 | internal: false, 74 | autoDelete: true 75 | }) 76 | 77 | tempChannel.close() 78 | 79 | return connection 80 | } 81 | 82 | async _incoming (message) { 83 | if (!message) { 84 | await throwAsException(new Error('Request reply consumer cancelled unexpectedly; this was most probably done via RabbitMQ\'s management panel')) 85 | } 86 | 87 | try { 88 | var content = JSON.parse(message.content.toString()) 89 | } catch (e) { 90 | console.error(e) 91 | } 92 | 93 | this._emitter.emit(`data-${message.properties.correlationId}`, message, ...content) 94 | } 95 | 96 | // Should we expose `name`, `exchange` and `url` publically? 97 | // we can use getters so they're still actually saved within 98 | // _options, but exposing them might be cool. 99 | get _exchange () { 100 | return this._options.exchange 101 | } 102 | } 103 | 104 | module.exports = Remit 105 | -------------------------------------------------------------------------------- /lib/Request.js: -------------------------------------------------------------------------------- 1 | const opentracing = require('opentracing') 2 | const CallableInstance = require('callable-instance') 3 | const EventEmitter = require('eventemitter3') 4 | const genUuid = require('../utils/genUuid') 5 | const parseEvent = require('../utils/parseEvent') 6 | const getStackLine = require('../utils/getStackLine') 7 | const throwAsException = require('../utils/throwAsException') 8 | const ms = require('ms') 9 | 10 | class Request extends CallableInstance { 11 | constructor (remit, opts = {}) { 12 | super('send') 13 | 14 | this._remit = remit 15 | 16 | this._emitter = new EventEmitter() 17 | this._remit._emitter = this._remit._emitter || new EventEmitter() 18 | 19 | this._timers = {} 20 | 21 | let parsedOpts = {} 22 | 23 | if (typeof opts === 'string') { 24 | parsedOpts.event = opts 25 | } else { 26 | parsedOpts = opts 27 | } 28 | 29 | if (!parsedOpts.event) { 30 | throw new Error('No/invalid event specified when creating a request') 31 | } 32 | 33 | this.options(parsedOpts) 34 | 35 | this._ready = this._setup(this._options) 36 | } 37 | 38 | on (...args) { 39 | // should we warn/block users when they try 40 | // to listen to an event that doesn't exist? 41 | this._emitter.on(...args) 42 | 43 | return this 44 | } 45 | 46 | fallback (data) { 47 | if (arguments.length === 0) { 48 | delete this._fallback 49 | } else { 50 | this._fallback = data 51 | } 52 | 53 | return this 54 | } 55 | 56 | options (opts = {}) { 57 | this._options = this._generateOptions(opts) 58 | 59 | return this 60 | } 61 | 62 | ready () { 63 | return this._ready 64 | } 65 | 66 | send (...args) { 67 | while (args.length < this._send.length) { 68 | args.push(undefined) 69 | } 70 | 71 | if (!this._remit._namespace.active) args[2] = true 72 | 73 | return this._remit._namespace.active 74 | ? this._send(...args) 75 | : this._remit._namespace.runAndReturn(this._send.bind(this, ...args)) 76 | } 77 | 78 | async _send (data = null, opts = {}, extendedCapture = false) { 79 | // parse the callsites here, as after the `await` 80 | // we'll get a different stack 81 | const callsites = getStackLine.capture(extendedCapture) 82 | await this._ready 83 | const now = new Date().getTime() 84 | const parsedOptions = this._generateOptions(opts) 85 | const trace = getStackLine.parse(callsites) 86 | const messageId = genUuid() 87 | 88 | const message = { 89 | mandatory: true, 90 | messageId: messageId, 91 | appId: this._remit._options.name, 92 | timestamp: now, 93 | headers: { 94 | trace 95 | }, 96 | correlationId: messageId, 97 | replyTo: 'amq.rabbitmq.reply-to' 98 | } 99 | 100 | if (parsedOptions.priority) { 101 | if (parsedOptions.priority > 10 || parsedOptions.priority < 0) { 102 | throw new Error(`Invalid priority "${parsedOptions.priority}" when making request`) 103 | } 104 | 105 | message.priority = parsedOptions.priority 106 | } 107 | 108 | let timeout = 30000 109 | let givenTimeout = Number(parsedOptions.timeout) 110 | if (!isNaN(givenTimeout)) timeout = givenTimeout 111 | 112 | if (timeout) message.expiration = timeout 113 | 114 | let parsedData 115 | let eventData = data 116 | 117 | // coerce data to `null` if undefined or an unparsable pure JS property. 118 | parsedData = JSON.stringify(data) 119 | 120 | if (typeof parsedData === 'undefined') { 121 | console.warn('[WARN] Remit request sent with unparsable JSON; this could be a function or an undefined variable. Data instead set to NULL.') 122 | 123 | // string here coerces to actual NULL once JSON.parse is performed 124 | parsedData = 'null' 125 | eventData = null 126 | } 127 | 128 | const parentContext = this._remit._namespace.get('context') 129 | 130 | const span = this._remit._tracer.startSpan(`Remit Request: ${parsedOptions.event}`, { 131 | tags: { 132 | 'remit.version': this._remit.version, 133 | [opentracing.Tags.SAMPLING_PRIORITY]: 1, 134 | [opentracing.Tags.COMPONENT]: 'remit', 135 | [opentracing.Tags.MESSAGE_BUS_DESTINATION]: parsedOptions.event, 136 | [opentracing.Tags.SPAN_KIND]: opentracing.Tags.SPAN_KIND_RPC_CLIENT, 137 | 'data.outgoing': eventData 138 | }, 139 | childOf: parentContext 140 | }) 141 | 142 | this._remit._tracer.inject(span.context(), opentracing.FORMAT_TEXT_MAP, message.headers.context) 143 | 144 | this._channel.publish( 145 | this._remit._exchange, 146 | parsedOptions.event, 147 | Buffer.from(parsedData), 148 | message 149 | ) 150 | 151 | const event = parseEvent(message, { 152 | routingKey: parsedOptions.event 153 | }, eventData) 154 | 155 | this._emitter.emit('sent', event) 156 | 157 | if (timeout) { 158 | this._setTimer(messageId, timeout, event) 159 | } 160 | 161 | return this._waitForResult(messageId, span) 162 | } 163 | 164 | _generateOptions (opts = {}) { 165 | const parsedOpts = {} 166 | 167 | if (opts.hasOwnProperty('timeout')) { 168 | parsedOpts.timeout = (typeof opts.timeout === 'string') 169 | ? ms(opts.timeout) 170 | : opts.timeout 171 | } 172 | 173 | return Object.assign({}, this._options || {}, opts, parsedOpts) 174 | } 175 | 176 | _setTimer (messageId, time, event) { 177 | this._timers[messageId] = setTimeout(() => { 178 | this._remit._emitter.emit(`timeout-${messageId}`, {}, { 179 | event: event, 180 | code: 'request_timedout', 181 | message: `Request timed out after no response for ${time}ms` 182 | }) 183 | }, time) 184 | } 185 | 186 | async _setup (opts = {}) { 187 | try { 188 | if (!this._remit._publishChannels[opts.event]) { 189 | const publishChannelP = this._remit._publishChannels[opts.event] = (async () => { 190 | const connection = await this._remit._connection 191 | return connection.createChannel() 192 | })() 193 | 194 | const publishChannel = await publishChannelP 195 | publishChannel.on('error', console.error) 196 | publishChannel.on('close', () => { 197 | throwAsException(new Error('Reply consumer died - this is most likely due to the RabbitMQ connection dying')) 198 | }) 199 | 200 | await publishChannel.consume( 201 | 'amq.rabbitmq.reply-to', 202 | this._remit._namespace.bind(this._remit._incoming.bind(this._remit)), 203 | { 204 | noAck: true, 205 | exclusive: true 206 | } 207 | ) 208 | } 209 | 210 | this._channel = await this._remit._publishChannels[opts.event] 211 | 212 | return this 213 | } catch (e) { 214 | await throwAsException(e) 215 | } 216 | } 217 | 218 | _waitForResult (messageId, span) { 219 | const types = ['data', 'timeout'] 220 | 221 | return new Promise((resolve, reject) => { 222 | const cleanUp = (message, err, result) => { 223 | clearTimeout(this._timers[messageId]) 224 | delete this._timers[messageId] 225 | 226 | types.forEach((type) => { 227 | this._remit._emitter.removeAllListeners(`${type}-${messageId}`) 228 | }) 229 | 230 | if (err) { 231 | this._emitter.emit('error', err) 232 | 233 | if (this.hasOwnProperty('_fallback')) { 234 | resolve(this._fallback) 235 | } else { 236 | reject(err) 237 | } 238 | 239 | span.setTag(opentracing.Tags.ERROR, true) 240 | span.setTag('data.incoming', err) 241 | } else { 242 | resolve(result) 243 | this._emitter.emit('success', result, message) 244 | span.setTag('data.incoming', result) 245 | } 246 | 247 | span.finish() 248 | 249 | this._emitter.emit( 250 | 'data', 251 | parseEvent(message.properties, message.fields, err || result) 252 | ) 253 | } 254 | 255 | types.forEach((type) => { 256 | this._remit._emitter.once(`${type}-${messageId}`, (message, ...args) => { 257 | cleanUp(message, ...args) 258 | if (type !== 'data') this._emitter.emit(type, ...args) 259 | }) 260 | }) 261 | }) 262 | } 263 | } 264 | 265 | module.exports = Request 266 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jpwilliams/remit", 3 | "version": "2.5.0", 4 | "description": "A small set of functionality used to create microservices that don't need to be aware of one-another's existence.", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "engines": { 8 | "node": ">=8" 9 | }, 10 | "scripts": { 11 | "coverage": "./node_modules/.bin/nyc ./node_modules/.bin/mocha --require test/bootstrap test/*.test.js test/**/*.test.js --exit && ./node_modules/.bin/nyc report --reporter=lcov", 12 | "test": "./node_modules/.bin/mocha --require test/bootstrap test/*.test.js test/**/*.test.js --exit", 13 | "travis": "./node_modules/.bin/nyc ./node_modules/.bin/_mocha --require test/bootstrap test/*.test.js test/**/*.test.js --exit && ./node_modules/.bin/nyc report --reporter=lcovonly && cat ./coverage/lcov.info | ./node_modules/.bin/coveralls && rm -rf ./coverage" 14 | }, 15 | "author": "Jack Williams ", 16 | "license": "MIT", 17 | "dependencies": { 18 | "amqplib": "^0.7.0", 19 | "callable-instance": "^2.0.0", 20 | "callsite": "^1.0.0", 21 | "cls-hooked": "^4.2.2", 22 | "eventemitter3": "^4.0.0", 23 | "generic-pool": "^3.7.1", 24 | "ms": "^2.1.1", 25 | "opentracing": "^0.14.3", 26 | "serialize-error": "^8.0.1", 27 | "ulid": "^2.3.0" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/jpwilliams/remit.git" 32 | }, 33 | "keywords": [ 34 | "micro", 35 | "service", 36 | "microservice", 37 | "microservices", 38 | "amqp", 39 | "rabbitmq", 40 | "zeromq", 41 | "rpc", 42 | "request", 43 | "response", 44 | "emit", 45 | "listen", 46 | "distributed", 47 | "events", 48 | "messaging" 49 | ], 50 | "bugs": { 51 | "url": "https://github.com/jpwilliams/remit/issues" 52 | }, 53 | "homepage": "https://github.com/jpwilliams/remit#readme", 54 | "files": [ 55 | "test", 56 | "lib", 57 | "utils" 58 | ], 59 | "devDependencies": { 60 | "chai": "^4.2.0", 61 | "coveralls": "^3.0.3", 62 | "mocha": "^8.0.1", 63 | "nyc": "^15.0.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/bootstrap.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | 3 | global.expect = chai.expect 4 | -------------------------------------------------------------------------------- /test/connection.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, expect */ 2 | describe('Connection', function () { 3 | describe('#connect', function () { 4 | it(`connection should throw`, done => { 5 | const url = 'amqp://not-a-real-host' 6 | const Remit = require('../') 7 | 8 | var originalException = process.listeners('uncaughtException').pop() 9 | 10 | process.removeListener('uncaughtException', originalException); 11 | process.once("uncaughtException", function (error) { 12 | recordedError = error 13 | expect(recordedError.errno).to.be.oneOf(['ENOTFOUND', 'EAI_AGAIN']) 14 | done() 15 | }) 16 | 17 | const remit = Remit({ url }) 18 | 19 | remit 20 | .request('foo') 21 | .send({}) 22 | 23 | process.nextTick(function () { 24 | process.listeners('uncaughtException').push(originalException) 25 | }) 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/emitter.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, expect */ 2 | const Remit = require('../') 3 | 4 | describe('Emitter', function () { 5 | describe('#object', function () { 6 | let remit 7 | 8 | before(function () { 9 | remit = Remit() 10 | }) 11 | 12 | it('should be a function', function () { 13 | expect(remit.emit).to.be.a('function') 14 | }) 15 | 16 | it('should expose "on" global function', function () { 17 | expect(remit.emit.on).to.be.a('function') 18 | }) 19 | }) 20 | 21 | describe('#return', function () { 22 | let remit, emitter 23 | 24 | before(function () { 25 | remit = Remit() 26 | emitter = remit.emit('foo') 27 | }) 28 | 29 | it('should throw if no event given', function () { 30 | expect(remit.emit.bind(null)).to.throw('No/invalid event specified when creating an emission') 31 | }) 32 | 33 | it('should return an Emitter', function () { 34 | expect(emitter).to.be.an.instanceof(remit.emit.Type) 35 | }) 36 | 37 | it('should be runnable (#send)', function () { 38 | expect(emitter).to.be.a('function') 39 | }) 40 | 41 | it('should expose an "on" function', function () { 42 | expect(emitter.on).to.be.a('function') 43 | }) 44 | 45 | it('should expose an "options" function', function () { 46 | expect(emitter.options).to.be.a('function') 47 | }) 48 | 49 | it('should expose a "ready" function', function () { 50 | expect(emitter.ready).to.be.a('function') 51 | }) 52 | 53 | it('should expose a "send" function', function () { 54 | expect(emitter.send).to.be.a('function') 55 | }) 56 | }) 57 | 58 | // for this section, assume all other parts of the library 59 | // work and only test emission features 60 | describe('#usage', function (done) { 61 | let listenRemit1, listenRemit2, emitRemit 62 | 63 | before(async function () { 64 | const remit1 = Remit({name: 'listen1'}) 65 | const remit2 = Remit({name: 'listen2'}) 66 | listenRemit1 = remit1.listen('emit-usage').handler(() => {}).start() 67 | listenRemit2 = remit2.listen('emit-usage').handler(() => {}).start() 68 | emitRemit = Remit({name: 'emitRemit'}) 69 | 70 | const [ r1, r2 ] = await Promise.all([listenRemit1, listenRemit2]) 71 | 72 | listenRemit1 = r1 73 | listenRemit2 = r2 74 | }) 75 | 76 | it('should parse timestrings and dates in a delay option', function () { 77 | const emit = emitRemit.emit('options-timestring-test') 78 | 79 | emit.options({delay: 20000}) 80 | expect(emit._options).to.have.property('delay', 20000) 81 | expect(emit._options).to.have.property('schedule', null) 82 | 83 | const d = new Date() 84 | d.setSeconds(d.getSeconds() + 15) 85 | emit.options({delay: d}) 86 | expect(emit._options).to.have.property('delay', null) 87 | expect(emit._options).to.have.property('schedule', d) 88 | 89 | emit.options({delay: '30m'}) 90 | expect(emit._options).to.have.property('delay', 1800000) 91 | expect(emit._options).to.have.property('schedule', null) 92 | 93 | emit.options({delay: '2s'}) 94 | expect(emit._options).to.have.property('delay', 2000) 95 | expect(emit._options).to.have.property('schedule', null) 96 | }) 97 | 98 | it('should return promise on send that resolves on sent') 99 | it('should emit "sent" on sending') 100 | it('should add priority if given in options before send') 101 | it('should add priority if given in options at send') 102 | it('should only set options at send for one emission') 103 | it('should pass `null` as data if JSON unparsable') 104 | it('should throw if demission queue dies before sending') 105 | it('should throw if failing to set up demission queue') 106 | it('should throw if invalid delay given') 107 | it('should throw if invalid schedule given') 108 | it('should not set delay if less than 1ms') 109 | it('should not schedule if less than 1ms') 110 | 111 | it('should emit to all listeners', async function () { 112 | const op = Promise.all([ 113 | waitForNext(listenRemit1), 114 | waitForNext(listenRemit2) 115 | ]) 116 | 117 | const sentEvent = await emitRemit 118 | .emit('emit-usage') 119 | .send({foo: 'bar'}) 120 | 121 | expect(sentEvent).to.have.property('eventType', 'emit-usage') 122 | expect(sentEvent).to.not.have.property('started') 123 | expect(sentEvent).to.have.property('eventId') 124 | expect(sentEvent).to.have.property('resource', 'emitRemit') 125 | expect(sentEvent).to.have.property('resourceTrace') 126 | expect(sentEvent).to.have.property('timestamp') 127 | expect(sentEvent.data.foo).to.equal('bar') 128 | // TODO test trace 129 | 130 | const events = await op 131 | 132 | expect(events).to.have.lengthOf(2) 133 | 134 | events.forEach((event) => { 135 | expect(event).to.have.property('started') 136 | expect(event.eventId).to.equal(sentEvent.eventId) 137 | expect(+event.timestamp).to.equal(+sentEvent.timestamp) 138 | expect(event.eventType).to.equal(sentEvent.eventType) 139 | expect(event.resource).to.equal(sentEvent.resource) 140 | expect(event.resourceTrace).to.equal(sentEvent.resourceTrace) 141 | }) 142 | }) 143 | 144 | it('should delay message by 1 seconds', async function () { 145 | this.slow(3000) 146 | 147 | const op = Promise.all([ 148 | waitForNext(listenRemit1), 149 | waitForNext(listenRemit2) 150 | ]) 151 | 152 | const sentEvent = await emitRemit 153 | .emit('emit-usage') 154 | .options({delay: 1000}) 155 | .send({bar: 'baz'}) 156 | 157 | expect(sentEvent).to.have.property('eventId') 158 | expect(sentEvent).to.not.have.property('started') 159 | expect(sentEvent).to.have.property('eventType', 'emit-usage') 160 | expect(sentEvent).to.have.property('resource', 'emitRemit') 161 | expect(sentEvent.data).to.have.property('bar', 'baz') 162 | expect(sentEvent).to.have.property('delay', 1000) 163 | expect(sentEvent).to.have.property('resourceTrace') 164 | expect(sentEvent).to.have.property('timestamp') 165 | 166 | const events = await op 167 | 168 | events.forEach((event) => { 169 | expect(event).to.have.property('started') 170 | expect(event.delay).to.equal(sentEvent.delay) 171 | expect(+event.timestamp).to.equal(+sentEvent.timestamp) 172 | expect(+event.started).to.be.above(+sentEvent.timestamp + sentEvent.delay) 173 | expect(event.eventId).to.equal(sentEvent.eventId) 174 | expect(event.eventType).to.equal(sentEvent.eventType) 175 | expect(event.resource).to.equal(sentEvent.resource) 176 | expect(event.data.bar).to.equal(sentEvent.data.bar) 177 | expect(event.resourceTrace).to.equal(sentEvent.resourceTrace) 178 | }) 179 | }) 180 | 181 | it('should schedule message for 2 seconds', async function () { 182 | this.timeout(5000) 183 | this.slow(5000) 184 | 185 | const op = Promise.all([ 186 | waitForNext(listenRemit1), 187 | waitForNext(listenRemit2) 188 | ]) 189 | 190 | let d = new Date() 191 | d.setSeconds(d.getSeconds() + 2) 192 | 193 | const sentEvent = await emitRemit 194 | .emit('emit-usage') 195 | .options({delay: d}) 196 | .send({bar: 'baz'}) 197 | 198 | expect(sentEvent).to.have.property('eventId') 199 | expect(sentEvent).to.not.have.property('started') 200 | expect(sentEvent).to.have.property('eventType', 'emit-usage') 201 | expect(sentEvent).to.have.property('resource', 'emitRemit') 202 | expect(sentEvent.data).to.have.property('bar', 'baz') 203 | expect(sentEvent).to.have.property('scheduled') 204 | expect(+sentEvent.scheduled).to.equal(+d) 205 | expect(sentEvent).to.have.property('resourceTrace') 206 | expect(sentEvent).to.have.property('timestamp') 207 | 208 | const events = await op 209 | 210 | events.forEach((event) => { 211 | expect(event).to.have.property('started') 212 | expect(event.schedule).to.equal(sentEvent.schedule) 213 | expect(+event.timestamp).to.equal(+sentEvent.timestamp) 214 | expect(+event.started).to.be.above(+sentEvent.scheduled) 215 | expect(event.eventId).to.equal(sentEvent.eventId) 216 | expect(event.eventType).to.equal(sentEvent.eventType) 217 | expect(event.resource).to.equal(sentEvent.resource) 218 | expect(event.data.bar).to.equal(sentEvent.data.bar) 219 | expect(event.resourceTrace).to.equal(sentEvent.resourceTrace) 220 | }) 221 | }) 222 | }) 223 | }) 224 | 225 | function waitForNext (instance) { 226 | return new Promise((resolve, reject) => { 227 | instance.on('data', (event) => { 228 | resolve(event) 229 | }) 230 | }) 231 | } 232 | -------------------------------------------------------------------------------- /test/endpoint.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, expect */ 2 | const { ulid } = require('ulid') 3 | const Remit = require('../') 4 | 5 | describe('Endpoint', function () { 6 | let remit 7 | 8 | before(function () { 9 | remit = Remit() 10 | }) 11 | 12 | describe('#object', function () { 13 | it('should be a function', function () { 14 | expect(remit.endpoint).to.be.a('function') 15 | }) 16 | 17 | it('should expose "on" global function', function () { 18 | expect(remit.endpoint.on).to.be.a('function') 19 | }) 20 | }) 21 | 22 | describe('#return', function () { 23 | let remit, endpoint 24 | 25 | before(function () { 26 | remit = Remit() 27 | endpoint = remit.endpoint('endpoint-test') 28 | }) 29 | 30 | it('should throw if given no event', function () { 31 | expect(remit.endpoint.bind()).to.throw('No/invalid event specified when creating an endpoint') 32 | }) 33 | 34 | it('should throw if given invalid event', function () { 35 | expect(remit.endpoint.bind(null, 123)).to.throw('No/invalid event specified when creating an endpoint') 36 | }) 37 | 38 | it('should throw if given invalid options object', function () { 39 | expect(remit.endpoint.bind(null, {})).to.throw('No/invalid event specified when creating an endpoint') 40 | }) 41 | 42 | it('should return an Endpoint', function () { 43 | expect(endpoint).to.be.an.instanceof(remit.endpoint.Type) 44 | }) 45 | 46 | it('should expose a "handler" function', function () { 47 | expect(endpoint.handler).to.be.a('function') 48 | }) 49 | 50 | it('should expose an "on" function', function () { 51 | expect(endpoint.on).to.be.a('function') 52 | }) 53 | 54 | it('should expose an "options" function', function () { 55 | expect(endpoint.options).to.be.a('function') 56 | }) 57 | 58 | it('should expose a "start" function', function () { 59 | expect(endpoint.start).to.be.a('function') 60 | }) 61 | }) 62 | 63 | describe('#usage', function () { 64 | let remit 65 | 66 | before(function () { 67 | remit = Remit({name: 'endpointRemit'}) 68 | }) 69 | 70 | it('should return same start promise if called multiple times', function () { 71 | const endpoint = remit.endpoint('start-test', () => {}) 72 | const start1 = endpoint.start() 73 | const start2 = endpoint.start() 74 | 75 | expect(start1).to.equal(start2) 76 | }) 77 | 78 | it('should assign new options over old ones', function () { 79 | const endpoint = remit.endpoint('options-test') 80 | expect(endpoint._options).to.have.property('event', 'options-test') 81 | expect(endpoint._options).to.have.property('queue', 'options-test') 82 | endpoint.options({queue: 'options-queue'}) 83 | expect(endpoint._options).to.have.property('event', 'options-test') 84 | expect(endpoint._options).to.have.property('queue', 'options-queue') 85 | }) 86 | 87 | it('should accept blank options (no changes)', function () { 88 | const endpoint = remit.endpoint('options-blank') 89 | expect(endpoint._options).to.have.property('event', 'options-blank') 90 | expect(endpoint._options).to.have.property('queue', 'options-blank') 91 | endpoint.options() 92 | expect(endpoint._options).to.have.property('event', 'options-blank') 93 | expect(endpoint._options).to.have.property('queue', 'options-blank') 94 | }) 95 | 96 | it('should not start consuming until `start` called') 97 | 98 | it('should throw if handler not specified on start', function () { 99 | const endpoint = remit.endpoint('no-handler-test') 100 | expect(endpoint.start.bind(endpoint)).to.throw('Trying to boot endpoint with no handler') 101 | }) 102 | 103 | it('should allow a handler to be set when creating', function () { 104 | const endpoint = remit.endpoint('handler-set-start', () => {}) 105 | expect(endpoint._handler).to.be.a('function') 106 | }) 107 | 108 | it('should allow a handler being set via the "handler" function', function () { 109 | const endpoint = remit.endpoint('handler-set-later') 110 | expect(endpoint._handler).to.equal(undefined) 111 | endpoint.handler(() => {}) 112 | expect(endpoint._handler).to.be.a('function') 113 | }) 114 | 115 | it('should throw if "handler" run with no handlers', function () { 116 | const endpoint = remit.endpoint('handler-no-err') 117 | expect(endpoint.handler.bind(endpoint)).to.throw('No handler(s) given when trying to set endpoint handler(s)') 118 | }) 119 | 120 | it('should return a promise on "start" that resolves when consuming', function () { 121 | this.slow(200) 122 | 123 | const endpoint = remit.endpoint('on-start', () => {}) 124 | const ret = endpoint.start() 125 | expect(ret).to.be.a('promise') 126 | 127 | return ret 128 | }) 129 | 130 | it('should return synchronous data', async function () { 131 | const endpoint = await remit 132 | .endpoint('return-1') 133 | .handler(() => { 134 | return 'foobar1' 135 | }) 136 | .start() 137 | 138 | const result = await remit.request('return-1')() 139 | expect(result).to.equal('foobar1') 140 | }) 141 | 142 | it('should return data via promises', async function () { 143 | const endpoint = await remit 144 | .endpoint('return-2') 145 | .handler(async () => { 146 | return 'foobar2' 147 | }) 148 | .start() 149 | 150 | const result = await remit.request('return-2')() 151 | expect(result).to.equal('foobar2') 152 | }) 153 | 154 | it('should return data via callback', async function () { 155 | const endpoint = await remit 156 | .endpoint('return-3') 157 | .handler((event, callback) => { 158 | callback(null, 'foobar3') 159 | }) 160 | .start() 161 | 162 | const result = await remit.request('return-3')() 163 | expect(result).to.equal('foobar3') 164 | }) 165 | 166 | it('should return synchronous error', async function () { 167 | const endpoint = await remit 168 | .endpoint('return-err-1') 169 | .handler(() => { 170 | throw 'fail1' 171 | }) 172 | .start() 173 | 174 | try { 175 | await remit.request('return-err-1')() 176 | throw new Error('Request succeeded') 177 | } catch (e) { 178 | expect(e).to.equal('fail1') 179 | } 180 | }) 181 | 182 | it('should return promise rejection', async function () { 183 | const endpoint = await remit 184 | .endpoint('return-err-2') 185 | .handler(async () => { 186 | throw 'fail2' 187 | }) 188 | .start() 189 | 190 | try { 191 | await remit.request('return-err-2')() 192 | throw new Error('Request succeeded') 193 | } catch (e) { 194 | expect(e).to.equal('fail2') 195 | } 196 | }) 197 | 198 | it('should return callback error', async function () { 199 | const endpoint = await remit 200 | .endpoint('return-err-3') 201 | .handler((event, callback) => { 202 | callback('fail3') 203 | }) 204 | .start() 205 | 206 | try { 207 | await remit.request('return-err-3')() 208 | throw new Error('Request succeeded') 209 | } catch (e) { 210 | expect(e).to.equal('fail3') 211 | } 212 | }) 213 | 214 | it('should return data early from synchronous middleware', async function () { 215 | const endpoint = await remit 216 | .endpoint('return-4') 217 | .handler(() => { 218 | return 'foobar4' 219 | }, () => { 220 | return 'fail' 221 | }) 222 | .start() 223 | 224 | const result = await remit.request('return-4')() 225 | expect(result).to.equal('foobar4') 226 | }) 227 | 228 | it('should return data early from promises middleware', async function () { 229 | const endpoint = await remit 230 | .endpoint('return-5') 231 | .handler(async () => { 232 | return 'foobar5' 233 | }, async () => { 234 | return 'fail' 235 | }) 236 | .start() 237 | 238 | const result = await remit.request('return-5')() 239 | expect(result).to.equal('foobar5') 240 | }) 241 | 242 | it('should return data early from callback middleware', async function () { 243 | const endpoint = await remit 244 | .endpoint('return-6') 245 | .handler((event, callback) => { 246 | callback(null, 'foobar6') 247 | }, (event, callback) => { 248 | callback(null, 'fail') 249 | }) 250 | .start() 251 | 252 | const result = await remit.request('return-6')() 253 | expect(result).to.equal('foobar6') 254 | }) 255 | 256 | it('should return error from synchronous middleware', async function () { 257 | const endpoint = await remit 258 | .endpoint('return-err-4') 259 | .handler(() => { 260 | throw 'fail4' 261 | }, () => { 262 | throw 'fail' 263 | }) 264 | .start() 265 | 266 | try { 267 | await remit.request('return-err-4')() 268 | throw new Error('Request succeeded') 269 | } catch (e) { 270 | expect(e).to.equal('fail4') 271 | } 272 | }) 273 | 274 | it('should return error from promises middleware', async function () { 275 | const endpoint = await remit 276 | .endpoint('return-err-5') 277 | .handler(async () => { 278 | throw 'fail5' 279 | }, async () => { 280 | throw 'fail' 281 | }) 282 | .start() 283 | 284 | try { 285 | await remit.request('return-err-5')() 286 | throw new Error('Request succeeded') 287 | } catch (e) { 288 | expect(e).to.equal('fail5') 289 | } 290 | }) 291 | 292 | it('should return error from callback middleware', async function () { 293 | const endpoint = await remit 294 | .endpoint('return-err-6') 295 | .handler((event, callback) => { 296 | callback('fail6') 297 | }, (event, callback) => { 298 | callback('fail') 299 | }) 300 | .start() 301 | 302 | try { 303 | await remit.request('return-err-6')() 304 | throw new Error('Request succeeded') 305 | } catch (e) { 306 | expect(e).to.equal('fail6') 307 | } 308 | }) 309 | 310 | it('should return final data from synchronous middleware', async function () { 311 | const endpoint = await remit 312 | .endpoint('return-7') 313 | .handler(() => { 314 | return 315 | }, () => { 316 | return 'foobar7' 317 | }) 318 | .start() 319 | 320 | const result = await remit.request('return-7')() 321 | expect(result).to.equal('foobar7') 322 | }) 323 | 324 | it('should return final data from promises middleware', async function () { 325 | const endpoint = await remit 326 | .endpoint('return-8') 327 | .handler(async () => { 328 | return 329 | }, async () => { 330 | return 'foobar8' 331 | }) 332 | .start() 333 | 334 | const result = await remit.request('return-8')() 335 | expect(result).to.equal('foobar8') 336 | }) 337 | 338 | it('should return final data from callback middleware', async function () { 339 | const endpoint = await remit 340 | .endpoint('return-9') 341 | .handler((event, callback) => { 342 | callback() 343 | }, (event, callback) => { 344 | callback(null, 'foobar9') 345 | }) 346 | .start() 347 | 348 | const result = await remit.request('return-9')() 349 | expect(result).to.equal('foobar9') 350 | }) 351 | 352 | it('should return final error from synchronous middleware', async function () { 353 | const endpoint = await remit 354 | .endpoint('return-err-7') 355 | .handler(() => { 356 | return 357 | }, () => { 358 | throw 'fail7' 359 | }) 360 | .start() 361 | 362 | try { 363 | await remit.request('return-err-7')() 364 | throw new Error('Request succeeded') 365 | } catch (e) { 366 | expect(e).to.equal('fail7') 367 | } 368 | }) 369 | 370 | it('should return final error from promises middleware', async function () { 371 | const endpoint = await remit 372 | .endpoint('return-err-8') 373 | .handler(async () => { 374 | return 375 | }, async () => { 376 | throw 'fail8' 377 | }) 378 | .start() 379 | 380 | try { 381 | await remit.request('return-err-8')() 382 | throw new Error('Request succeeded') 383 | } catch (e) { 384 | expect(e).to.equal('fail8') 385 | } 386 | }) 387 | 388 | it('should return final error from callback middleware', async function () { 389 | const endpoint = await remit 390 | .endpoint('return-err-9') 391 | .handler((event, callback) => { 392 | callback() 393 | }, (event, callback) => { 394 | callback('fail9') 395 | }) 396 | .start() 397 | 398 | try { 399 | await remit.request('return-err-9')() 400 | throw new Error('Request succeeded') 401 | } catch (e) { 402 | expect(e).to.equal('fail9') 403 | } 404 | }) 405 | 406 | it('should instantly return values in handlers', async function () { 407 | await remit 408 | .endpoint('handler-values') 409 | .handler('handler-values-foobar') 410 | .start() 411 | 412 | const result = await remit.request('handler-values')() 413 | expect(result).to.equal('handler-values-foobar') 414 | }) 415 | 416 | it('should instantly return even falsey values in handlers', async function () { 417 | await remit 418 | .endpoint('handler-values-falsey') 419 | .handler(0) 420 | .start() 421 | 422 | const result = await remit.request('handler-values-falsey')() 423 | expect(result).to.equal(0) 424 | }) 425 | 426 | it('should pass the same `event` to every handler', async function () { 427 | const endpoint = await remit 428 | .endpoint('same-event') 429 | .handler((event) => { 430 | event.custom = 'blamblam' 431 | }, (event) => { 432 | expect(event.custom).to.equal('blamblam') 433 | 434 | return event.custom 435 | }) 436 | .start() 437 | 438 | const result = await remit.request('same-event')() 439 | expect(result).to.equal('blamblam') 440 | }) 441 | 442 | it('should allow changing handlers realtime', async function () { 443 | const endpoint = await remit 444 | .endpoint('changing-handlers') 445 | .handler(() => 'foobar') 446 | .start() 447 | 448 | const req = await remit.request('changing-handlers').ready() 449 | let res = await req() 450 | expect(res).to.equal('foobar') 451 | endpoint.handler(() => 'bazqux') 452 | res = await req() 453 | expect(res).to.equal('bazqux') 454 | }) 455 | 456 | it('should throw if consumer cancelled remotely') 457 | 458 | it('should do nothing if pause requested and not started', async function () { 459 | const endpoint = remit.endpoint('foo') 460 | 461 | const p = endpoint.pause() 462 | expect(p).to.be.a('promise') 463 | 464 | const res = await p 465 | expect(res).to.equal(endpoint) 466 | 467 | expect(endpoint._started).to.equal(undefined) 468 | expect(endpoint._paused).to.equal(undefined) 469 | }) 470 | 471 | it('should run start if resume requested and not started', async function () { 472 | const endpoint = remit 473 | .endpoint('foo') 474 | .handler(() => {}) 475 | 476 | const p = endpoint.resume() 477 | expect(p).to.be.a('promise') 478 | 479 | const res = await p 480 | expect(res).to.equal(endpoint) 481 | expect(endpoint._started).to.be.a('promise') 482 | }) 483 | 484 | it('should pause consumption if running and pause requested', async function () { 485 | this.timeout(6000) 486 | this.slow(6000) 487 | const queue = ulid() 488 | 489 | let hits = 0 490 | const req = remit 491 | .request(queue) 492 | .options({ 493 | timeout: 2000 494 | }) 495 | 496 | const endpoint = await remit 497 | .endpoint(queue) 498 | .handler(() => { 499 | hits++ 500 | }) 501 | .start() 502 | 503 | expect(endpoint._started).to.be.a('promise') 504 | 505 | await req() 506 | expect(hits).to.equal(1) 507 | 508 | const p = endpoint.pause() 509 | expect(p).to.be.a('promise') 510 | expect(endpoint._paused).to.be.a('promise') 511 | 512 | let errorCaught = false 513 | const res = await p 514 | expect(res).to.equal(endpoint) 515 | 516 | try { 517 | await req() 518 | } catch (e) { 519 | errorCaught = true 520 | expect(e).to.be.an('object') 521 | expect(e).to.have.property('code', 'request_timedout') 522 | expect(e).to.have.property('message', 'Request timed out after no response for 2000ms') 523 | } 524 | 525 | expect(errorCaught).to.equal(true) 526 | expect(hits).to.equal(1) 527 | }) 528 | 529 | it('should resume consumption if paused and resume requested', async function () { 530 | this.timeout(6000) 531 | this.slow(6000) 532 | const queue = ulid() 533 | 534 | let hits = 0 535 | const req = remit 536 | .request(queue) 537 | .options({ 538 | timeout: 2000 539 | }) 540 | 541 | const endpoint = await remit 542 | .endpoint(queue) 543 | .handler(() => { 544 | hits++ 545 | }) 546 | .start() 547 | 548 | const res = await endpoint.pause() 549 | expect(res).to.equal(endpoint) 550 | 551 | let errorCaught = false 552 | 553 | try { 554 | await req() 555 | } catch (e) { 556 | errorCaught = true 557 | expect(e).to.be.an('object') 558 | expect(e).to.have.property('code', 'request_timedout') 559 | expect(e).to.have.property('message', 'Request timed out after no response for 2000ms') 560 | } 561 | 562 | expect(errorCaught).to.equal(true) 563 | expect(hits).to.equal(0) 564 | 565 | // set a timeout to give rabbitmq time to clear 566 | // the old message 567 | await new Promise((resolve, reject) => { 568 | setTimeout(async () => { 569 | const p = endpoint.resume() 570 | expect(p).to.be.a('promise') 571 | 572 | const resumeRes = await p 573 | expect(resumeRes).to.equal(endpoint) 574 | await req() 575 | expect(hits).to.equal(1) 576 | resolve() 577 | }, 1000) 578 | }) 579 | }) 580 | 581 | it('should return the same promise if pause requested multiple times', async function () { 582 | const queue = ulid() 583 | 584 | const endpoint = await remit 585 | .endpoint(queue) 586 | .handler(() => {}) 587 | .start() 588 | 589 | const p1 = endpoint.pause() 590 | const p2 = endpoint.pause() 591 | 592 | expect(p1).to.be.a('promise') 593 | expect(p2).to.be.a('promise') 594 | expect(p1).to.equal(p2) 595 | 596 | const res = await p1 597 | expect(res).to.equal(endpoint) 598 | }) 599 | 600 | it('should return the same promise if resume requested multiple times', async function () { 601 | const queue = ulid() 602 | 603 | const endpoint = await remit 604 | .endpoint(queue) 605 | .handler(() => {}) 606 | .start() 607 | 608 | await endpoint.pause() 609 | 610 | const p1 = endpoint.resume() 611 | const p2 = endpoint.resume() 612 | 613 | expect(p1).to.be.a('promise') 614 | expect(p2).to.be.a('promise') 615 | expect(p1).to.equal(p2) 616 | 617 | const res = await p1 618 | expect(res).to.equal(endpoint) 619 | }) 620 | }) 621 | 622 | it('should not throw when message has no headers property') 623 | }) 624 | -------------------------------------------------------------------------------- /test/exports.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, expect */ 2 | const Remit = require('../') 3 | const version = require('../package.json').version 4 | 5 | describe('Remit', function () { 6 | let remit 7 | 8 | before(function () { 9 | remit = Remit() 10 | }) 11 | 12 | describe('#object', function () { 13 | it('should export the remit function', function () { 14 | expect(Remit).to.be.a('function') 15 | }) 16 | 17 | it('should export a version', function () { 18 | expect(remit).to.have.property('version', version) 19 | }) 20 | 21 | it('should export "listen" function', function () { 22 | expect(remit.listen).to.be.a('function') 23 | }) 24 | 25 | it('should export "emit" function', function () { 26 | expect(remit.emit).to.be.a('function') 27 | }) 28 | 29 | it('should export "endpoint" function', function () { 30 | expect(remit.endpoint).to.be.a('function') 31 | }) 32 | 33 | it('should export "request" function', function () { 34 | expect(remit.request).to.be.a('function') 35 | }) 36 | 37 | it('should export "on" function', function () { 38 | expect(remit.on).to.be.a('function') 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /test/listener.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, expect */ 2 | const Remit = require('../') 3 | 4 | describe('Listener', function () { 5 | let remit 6 | 7 | before(function () { 8 | remit = Remit() 9 | }) 10 | 11 | describe('#object', function () { 12 | it('should be a function', function () { 13 | expect(remit.listen).to.be.a('function') 14 | }) 15 | 16 | it('should expose "on" global function', function () { 17 | expect(remit.listen.on).to.be.a('function') 18 | }) 19 | }) 20 | 21 | describe('#return', function () { 22 | let listener 23 | 24 | before(async function () { 25 | listener = remit.listen('foo') 26 | }) 27 | 28 | it('should return a Listener', function () { 29 | expect(listener).to.be.an.instanceof(remit.listen.Type) 30 | }) 31 | 32 | it('should expose a "handler" function', function () { 33 | expect(listener.handler).to.be.a('function') 34 | }) 35 | 36 | it('should expose an "on" function', function () { 37 | expect(listener.on).to.be.a('function') 38 | }) 39 | 40 | it('should expose an "options" function', function () { 41 | expect(listener.options).to.be.a('function') 42 | }) 43 | 44 | it('should expose a "start" function', function () { 45 | expect(listener.start).to.be.a('function') 46 | }) 47 | 48 | it('should not throw when message has no headers property') 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/request.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before, expect */ 2 | const Remit = require('../') 3 | 4 | describe('Request', function () { 5 | let remit 6 | 7 | before(function () { 8 | remit = Remit() 9 | }) 10 | 11 | describe('#object', function () { 12 | it('should be a function', function () { 13 | expect(remit.request).to.be.a('function') 14 | }) 15 | it('should expose "on" global function', function () { 16 | expect(remit.endpoint.on).to.be.a('function') 17 | }) 18 | }) 19 | 20 | describe('#return', function () { 21 | let remit, request 22 | 23 | before(function () { 24 | remit = Remit() 25 | request = remit.request('request-test') 26 | }) 27 | 28 | it('should throw if no event given', function () { 29 | expect(remit.request).to.throw('No/invalid event specified when creating a request') 30 | }) 31 | 32 | it('should throw if given no event in options object', function () { 33 | expect(remit.request.bind(remit, {})).to.throw('No/invalid event specified when creating a request') 34 | }) 35 | 36 | it('should allow options to be set on first run', function () { 37 | const req = remit.request({ 38 | event: 'tester-123', 39 | timeout: 2000 40 | }) 41 | 42 | expect(req._options).to.have.property('event', 'tester-123') 43 | expect(req._options).to.have.property('timeout', 2000) 44 | }) 45 | 46 | it('should return a Request', function () { 47 | expect(request).to.be.an.instanceof(remit.request.Type) 48 | }) 49 | 50 | it('should be runnable (#send)', function () { 51 | expect(request).to.be.a('function') 52 | }) 53 | 54 | it('should expose an "on" function', function () { 55 | expect(request.on).to.be.a('function') 56 | }) 57 | it('should expose a "fallback" function', function () { 58 | expect(request.fallback).to.be.a('function') 59 | }) 60 | it('should expose an "options" function', function () { 61 | expect(request.options).to.be.a('function') 62 | }) 63 | it('should expose a "ready" function', function () { 64 | expect(request.ready).to.be.a('function') 65 | }) 66 | it('should expose a "send" function', function () { 67 | expect(request.send).to.be.a('function') 68 | }) 69 | }) 70 | 71 | describe('#usage', function () { 72 | let remit 73 | 74 | before(function () { 75 | remit = Remit({name: 'requestRemit'}) 76 | }) 77 | 78 | it('should assign new options over old ones', function () { 79 | const request = remit.request('options-test') 80 | expect(request._options).to.have.property('event', 'options-test') 81 | request.options({event: 'options-queue'}) 82 | expect(request._options).to.have.property('event', 'options-queue') 83 | }) 84 | 85 | it('should parse timestrings in a timeout option', function () { 86 | const request = remit.request('options-timestring-test') 87 | request.options({timeout: '30m'}) 88 | expect(request._options).to.have.property('timeout', 1800000) 89 | request.options({timeout: '2s'}) 90 | expect(request._options).to.have.property('timeout', 2000) 91 | }) 92 | 93 | it('should return \'ready\' promise when request is ready to be used', function () { 94 | this.slow(200) 95 | 96 | const request = remit.request('on-start') 97 | const ret = request.ready() 98 | expect(ret).to.be.a('promise') 99 | 100 | return ret 101 | }) 102 | 103 | it('should allow a fallback to be set', function () { 104 | const request = remit.request('fallback-test') 105 | expect(request).to.not.have.property('_fallback') 106 | request.fallback(123) 107 | expect(request).to.have.property('_fallback', 123) 108 | }) 109 | 110 | it('should allow a falsy fallback to be set', function () { 111 | const request = remit.request('fallback-test-falsy') 112 | expect(request).to.not.have.property('_fallback') 113 | request.fallback(null) 114 | expect(request).to.have.property('_fallback', null) 115 | }) 116 | 117 | it('should allow a fallback to be unset', function () { 118 | const request = remit.request('fallback-test-unset') 119 | expect(request).to.not.have.property('_fallback') 120 | request.fallback('testfallback') 121 | expect(request).to.have.property('_fallback', 'testfallback') 122 | request.fallback() 123 | expect(request).to.not.have.property('_fallback') 124 | }) 125 | 126 | it('should throw if sent with invalid priority', async function () { 127 | const request = remit.request('invalid-priority') 128 | request.options({priority: 11}) 129 | 130 | try { 131 | await request() 132 | throw new Error('Should have failed with invalid priority') 133 | } catch (e) { 134 | expect(e.message).to.equal('Invalid priority "11" when making request') 135 | } 136 | }) 137 | 138 | it('should timeout in configurable times', async function () { 139 | this.slow(2000) 140 | 141 | const request = remit.request('timeout-test') 142 | request.options({timeout: 1000}) 143 | 144 | try { 145 | await request() 146 | throw new Error('Request should not have succeeded') 147 | } catch (e) { 148 | expect(e.message).to.equal('Request timed out after no response for 1000ms') 149 | } 150 | }) 151 | 152 | it('should return fallback if timing out and fallback set') 153 | it('should expire from queue after same time as timeout') 154 | it('should send NULL if given unparsable data') 155 | }) 156 | }) 157 | -------------------------------------------------------------------------------- /test/utils/CallableWrapper.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpwilliams/remit/9cbc7558f32354f30572198f5457dfd883b3b346/test/utils/CallableWrapper.test.js -------------------------------------------------------------------------------- /test/utils/asyncWaterfall.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpwilliams/remit/9cbc7558f32354f30572198f5457dfd883b3b346/test/utils/asyncWaterfall.test.js -------------------------------------------------------------------------------- /test/utils/genUuid.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpwilliams/remit/9cbc7558f32354f30572198f5457dfd883b3b346/test/utils/genUuid.test.js -------------------------------------------------------------------------------- /test/utils/generateConnectionOptions.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpwilliams/remit/9cbc7558f32354f30572198f5457dfd883b3b346/test/utils/generateConnectionOptions.test.js -------------------------------------------------------------------------------- /test/utils/getStackLine.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpwilliams/remit/9cbc7558f32354f30572198f5457dfd883b3b346/test/utils/getStackLine.test.js -------------------------------------------------------------------------------- /test/utils/handlerWrapper.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpwilliams/remit/9cbc7558f32354f30572198f5457dfd883b3b346/test/utils/handlerWrapper.test.js -------------------------------------------------------------------------------- /test/utils/parseAmqpUrl.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before */ 2 | const Remit = require('../../') 3 | const parse = require('../../utils/parseAmqpUrl') 4 | 5 | describe('parseAmqpUrl', function () { 6 | it('should add protocol if missing', function () { 7 | expect( 8 | parse('localhost') 9 | ).to.equal('amqp://localhost?frameMax=0x1000&heartbeat=15') 10 | }) 11 | 12 | it('should throw if invalid protocol given', function () { 13 | expect( 14 | parse.bind(null, 'amq://localhost') 15 | ).to.throw('Incorrect protocol') 16 | }) 17 | 18 | it('should overwrite and merge query strings', function () { 19 | expect( 20 | parse('localhost?here=we&go=whoosh&heartbeat=30') 21 | ).to.equal('amqp://localhost?frameMax=0x1000&heartbeat=30&here=we&go=whoosh') 22 | }) 23 | 24 | it('should match with username and password', function () { 25 | expect( 26 | parse('amqp://user:pass@localhost:5672') 27 | ).to.equal('amqp://user:pass@localhost:5672?frameMax=0x1000&heartbeat=15') 28 | }) 29 | 30 | it('should match with username and password with options', function () { 31 | expect( 32 | parse('amqp://user:pass@localhost:5672?heartbeat=30') 33 | ).to.equal('amqp://user:pass@localhost:5672?frameMax=0x1000&heartbeat=30') 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /test/utils/parseEvent.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpwilliams/remit/9cbc7558f32354f30572198f5457dfd883b3b346/test/utils/parseEvent.test.js -------------------------------------------------------------------------------- /test/utils/serializeData.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpwilliams/remit/9cbc7558f32354f30572198f5457dfd883b3b346/test/utils/serializeData.test.js -------------------------------------------------------------------------------- /utils/CallableWrapper.js: -------------------------------------------------------------------------------- 1 | const CallableInstance = require('callable-instance') 2 | const EventEmitter = require('eventemitter3') 3 | 4 | const listeners = [ 5 | 'data', 6 | 'sent', 7 | 'timeout', 8 | 'error', 9 | 'success' 10 | ] 11 | 12 | class CallableWrapper extends CallableInstance { 13 | constructor (remit, Type) { 14 | super('_create') 15 | 16 | this.remit = remit 17 | this.Type = Type 18 | this._emitter = new EventEmitter() 19 | } 20 | 21 | on (...args) { 22 | this._emitter.on(...args) 23 | 24 | return this 25 | } 26 | 27 | _create (...args) { 28 | const ret = new this.Type(this.remit, ...args) 29 | 30 | for (const k of listeners) { 31 | ret.on(k, (...x) => this._emitter.emit(k, ...x)) 32 | } 33 | 34 | return ret 35 | } 36 | } 37 | 38 | module.exports = CallableWrapper 39 | -------------------------------------------------------------------------------- /utils/ChannelPool.js: -------------------------------------------------------------------------------- 1 | const genericPool = require('generic-pool') 2 | 3 | function ChannelPool (connection) { 4 | return genericPool.createPool({ 5 | create: async () => { 6 | const con = await connection 7 | const channel = await con.createChannel() 8 | channel.on('error', () => {}) 9 | channel.on('close', () => console.log('Worker channel closed')) 10 | 11 | return channel 12 | }, 13 | 14 | destroy: channel => channel.close() 15 | }, { 16 | min: 5, 17 | max: 10 18 | }) 19 | } 20 | 21 | module.exports = ChannelPool 22 | -------------------------------------------------------------------------------- /utils/asyncWaterfall.js: -------------------------------------------------------------------------------- 1 | const { serializeError } = require('serialize-error') 2 | 3 | function waterfall (...fns) { 4 | return async function (event) { 5 | let result 6 | 7 | try { 8 | for (const fn of fns) { 9 | result = await fn(event) 10 | 11 | if (result !== undefined) { 12 | return [null, result] 13 | } 14 | } 15 | 16 | return [null, result] 17 | } catch (e) { 18 | console.error(e) 19 | const err = (e instanceof Error) ? serializeError(e) : e 20 | 21 | return [err, null] 22 | } 23 | } 24 | } 25 | 26 | module.exports = waterfall 27 | -------------------------------------------------------------------------------- /utils/genUuid.js: -------------------------------------------------------------------------------- 1 | const ulid = require('ulid').ulid 2 | 3 | function genUuid () { 4 | return ulid() 5 | } 6 | 7 | module.exports = genUuid 8 | -------------------------------------------------------------------------------- /utils/generateConnectionOptions.js: -------------------------------------------------------------------------------- 1 | const os = require('os') 2 | const packageJson = require('../package.json') 3 | 4 | function generateConnectionOptions (name) { 5 | return { 6 | noDelay: true, 7 | clientProperties: { 8 | connection_name: name, 9 | powered_by: `${packageJson.name}@${packageJson.version} (${packageJson.repository.url.substr(0, packageJson.repository.url.length - 4)}/tree/${packageJson.version})`, 10 | repository: packageJson.repository.url, 11 | package: `https://www.npmjs.com/package/${packageJson.name}`, 12 | host: { 13 | name: `${os.userInfo().username}@${os.hostname()}`, 14 | platform: `${os.type()}@${os.release()}`, 15 | pid: process.pid, 16 | node: process.version 17 | } 18 | } 19 | } 20 | } 21 | 22 | module.exports = generateConnectionOptions 23 | -------------------------------------------------------------------------------- /utils/getStackLine.js: -------------------------------------------------------------------------------- 1 | const stack = require('callsite') 2 | 3 | module.exports = { 4 | capture: function capture (extendedCapture) { 5 | return stack().slice(...(extendedCapture ? [6, 8] : [3, 5])) 6 | }, 7 | 8 | parse: function parse (callsites) { 9 | let callsite 10 | 11 | if (callsites[1]) { 12 | const filename = callsites[0].getFileName() 13 | 14 | if (filename && filename.substr(-39) === 'node_modules/callable-instance/index.js') { 15 | callsite = callsites[1] 16 | } else { 17 | callsite = callsites[0] 18 | } 19 | } else { 20 | callsite = callsites[0] 21 | } 22 | 23 | return `${callsite.getFunctionName() || 'Object.'} (${callsite.getFileName()}:${callsite.getLineNumber()}:${callsite.getColumnNumber()})` 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /utils/handlerWrapper.js: -------------------------------------------------------------------------------- 1 | function handlerWrapper (fn) { 2 | return (event) => { 3 | return new Promise((resolve, reject) => { 4 | try { 5 | if (typeof fn !== 'function') { 6 | return resolve(fn) 7 | } 8 | 9 | const r = fn(event, (err, data) => { 10 | if (err) { 11 | reject(err) 12 | } else { 13 | resolve(data) 14 | } 15 | }) 16 | 17 | // if they've mapped a callback, _always_ wait for 18 | // the callback. 19 | // this helps clear up issues where someone has 20 | // created an `async` function to use Promises 21 | // but still mapped the callback to use later. 22 | // JS is full of mixing these types, so we should 23 | // be nice and clear on how they're handled 24 | if (fn.length < 2) { 25 | if (r && r.then && typeof r.then === 'function') { 26 | // is a promise 27 | r.then(resolve).catch(reject) 28 | } else { 29 | // is synchronous 30 | resolve(r) 31 | } 32 | } 33 | 34 | // if we're here, it's using a callback, so we'll 35 | // wait 36 | } catch (e) { 37 | reject(e) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | module.exports = handlerWrapper 44 | -------------------------------------------------------------------------------- /utils/parseAmqpUrl.js: -------------------------------------------------------------------------------- 1 | const url = require('url') 2 | 3 | function parseUrl (input) { 4 | const parsedUrl = url.parse(input, true) 5 | 6 | if (parsedUrl.protocol) { 7 | if (parsedUrl.protocol !== 'amqp:' && parsedUrl.protocol !== 'amqps:') { 8 | throw new Error('Incorrect protocol') 9 | } 10 | } else { 11 | if (parsedUrl.pathname && !parsedUrl.host) { 12 | parsedUrl.host = parsedUrl.pathname 13 | parsedUrl.path = null 14 | parsedUrl.pathname = null 15 | parsedUrl.href = null 16 | } 17 | 18 | parsedUrl.protocol = 'amqp:' 19 | parsedUrl.slashes = true 20 | } 21 | 22 | // Overwrite query parameters, as we don't want to allow 23 | // any outside specification. 24 | parsedUrl.query = Object.assign({}, { 25 | // Maximum permissible size of a frame (in bytes) 26 | // to negotiate with clients. Setting to 0 means 27 | // "unlimited" but will trigger a bug in some QPid 28 | // clients. Setting a larger value may improve 29 | // throughput; setting a smaller value may improve 30 | // latency. 31 | // I default it to 0x1000, i.e. 4kb, which is the 32 | // allowed minimum, will fit many purposes, and not 33 | // chug through Node.JS's buffer pooling. 34 | // 35 | // frameMax: '0x20000', // 131,072 (128kb) 36 | // 37 | frameMax: '0x1000', // 4,096 (4kb) 38 | heartbeat: '15' // Frequent hearbeat 39 | }, parsedUrl.query) 40 | 41 | // `search` overwrites `query` if defined 42 | parsedUrl.search = '' 43 | 44 | return url.format(parsedUrl) 45 | } 46 | 47 | module.exports = parseUrl 48 | -------------------------------------------------------------------------------- /utils/parseEvent.js: -------------------------------------------------------------------------------- 1 | function parseEvent (properties = {}, fields = {}, data, opts = {}) { 2 | const event = { 3 | eventId: properties.messageId, 4 | eventType: fields.routingKey, 5 | resource: properties.appId, 6 | data: data 7 | } 8 | 9 | if (opts.isReceiver) { 10 | event.started = new Date() 11 | } 12 | 13 | if (properties.headers) { 14 | if (properties.headers.uuid) { 15 | event.eventId = properties.headers.uuid 16 | } 17 | 18 | if (properties.headers.scheduled) { 19 | event.scheduled = new Date(properties.headers.scheduled) 20 | } 21 | 22 | if (properties.headers.delay) { 23 | event.delay = properties.headers.delay 24 | } 25 | 26 | if (properties.headers.trace) { 27 | event.resourceTrace = properties.headers.trace 28 | } 29 | } 30 | 31 | if (properties.timestamp) { 32 | let timestamp = properties.timestamp 33 | 34 | if (timestamp.toString().length === 10) { 35 | timestamp *= 1000 36 | } 37 | 38 | event.timestamp = new Date(timestamp) 39 | } 40 | 41 | return event 42 | } 43 | 44 | module.exports = parseEvent 45 | -------------------------------------------------------------------------------- /utils/serializeData.js: -------------------------------------------------------------------------------- 1 | function serializeData (data) { 2 | return JSON.stringify(data.slice(0, 2)) 3 | } 4 | 5 | module.exports = serializeData 6 | -------------------------------------------------------------------------------- /utils/throwAsException.js: -------------------------------------------------------------------------------- 1 | // make this exception synchronous so we exit 2 | // the node process without having to add a 3 | // adding a listener to unhandledRejection 4 | function throwAsException (e) { 5 | // returns a promise to stop returning wrong error 6 | return new Promise(() => { 7 | process.nextTick(() => { 8 | throw e 9 | }) 10 | }) 11 | } 12 | 13 | module.exports = throwAsException --------------------------------------------------------------------------------