├── .dependabot └── config.yml ├── .editorconfig ├── .github ├── CODEOWNERS └── workflows │ └── sync-repo-labels.yml ├── .gitignore ├── .jscsrc ├── .jshintignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── Makefile.node ├── README.md ├── example ├── basic │ └── index.js ├── connect-basic │ └── index.js ├── connect-varying-rates │ └── index.js └── query │ └── index.js ├── lib └── pacer.js ├── package.json └── test └── unit ├── lib └── pacer.js ├── mock ├── redis.js └── underscore.js └── setup.js /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | update_configs: 3 | - package_manager: javascript 4 | directory: / 5 | update_schedule: live 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | indent_style = spaces 8 | indent_size = 4 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.json] 13 | insert_final_newline = false 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @rowanmanning 2 | -------------------------------------------------------------------------------- /.github/workflows/sync-repo-labels.yml: -------------------------------------------------------------------------------- 1 | on: [issues, pull_request] 2 | name: Sync repo labels 3 | jobs: 4 | sync: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: rowanmanning/github-labels@v1 8 | with: 9 | github-token: ${{ secrets.GITHUB_TOKEN }} 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowDanglingUnderscores": true, 3 | "disallowEmptyBlocks": true, 4 | "disallowImplicitTypeConversion": [ 5 | "binary", 6 | "boolean", 7 | "numeric", 8 | "string" 9 | ], 10 | "disallowMixedSpacesAndTabs": true, 11 | "disallowMultipleSpaces": true, 12 | "disallowMultipleVarDecl": true, 13 | "disallowNewlineBeforeBlockStatements": true, 14 | "disallowQuotedKeysInObjects": true, 15 | "disallowSpaceAfterObjectKeys": true, 16 | "disallowSpaceAfterPrefixUnaryOperators": true, 17 | "disallowSpaceBeforeComma": true, 18 | "disallowSpaceBeforeSemicolon": true, 19 | "disallowSpacesInCallExpression": true, 20 | "disallowTrailingComma": true, 21 | "disallowTrailingWhitespace": true, 22 | "disallowYodaConditions": true, 23 | "maximumLineLength": 100, 24 | "requireBlocksOnNewline": true, 25 | "requireCamelCaseOrUpperCaseIdentifiers": true, 26 | "requireCapitalizedConstructors": true, 27 | "requireCommaBeforeLineBreak": true, 28 | "requireCurlyBraces": true, 29 | "requireDotNotation": true, 30 | "requireFunctionDeclarations": true, 31 | "requireKeywordsOnNewLine": [ 32 | "else" 33 | ], 34 | "requireLineBreakAfterVariableAssignment": true, 35 | "requireLineFeedAtFileEnd": true, 36 | "requireObjectKeysOnNewLine": true, 37 | "requireParenthesesAroundIIFE": true, 38 | "requireSemicolons": true, 39 | "requireSpaceAfterBinaryOperators": true, 40 | "requireSpaceAfterKeywords": true, 41 | "requireSpaceAfterLineComment": true, 42 | "requireSpaceBeforeBinaryOperators": true, 43 | "requireSpaceBeforeBlockStatements": true, 44 | "requireSpaceBeforeObjectValues": true, 45 | "requireSpaceBetweenArguments": true, 46 | "requireSpacesInConditionalExpression": true, 47 | "requireSpacesInForStatement": true, 48 | "requireSpacesInFunction": { 49 | "beforeOpeningRoundBrace": true, 50 | "beforeOpeningCurlyBrace": true 51 | }, 52 | "validateIndentation": 4, 53 | "validateLineBreaks": "LF", 54 | "validateParameterSeparator": ", ", 55 | "validateQuoteMarks": "'", 56 | 57 | "excludeFiles": [ 58 | "coverage", 59 | "node_modules" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "after": true, 4 | "afterEach": true, 5 | "before": true, 6 | "beforeEach": true, 7 | "describe": true, 8 | "it": true 9 | }, 10 | "latedef": "nofunc", 11 | "maxparams": 5, 12 | "maxdepth": 2, 13 | "maxstatements": 8, 14 | "maxcomplexity": 5, 15 | "node": true, 16 | "strict": true, 17 | "unused": true 18 | } 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | # Build matrix 3 | language: node_js 4 | matrix: 5 | include: 6 | 7 | # Run linter once 8 | - node_js: '7' 9 | env: LINT=true 10 | 11 | # Run tests 12 | - node_js: '0.10' 13 | - node_js: '0.12' 14 | - node_js: '4' 15 | - node_js: '5' 16 | - node_js: '6' 17 | - node_js: '7' 18 | 19 | # Restrict builds on branches 20 | branches: 21 | only: 22 | - master 23 | - /^\d+\.\d+\.\d+$/ 24 | 25 | # Before install 26 | before_install: 27 | - npm install coveralls 28 | 29 | # Build script 30 | script: 31 | - 'if [ $LINT ]; then make verify; fi' 32 | - 'if [ ! $LINT ]; then make test; fi' 33 | - 'if [ ! $LINT ]; then cat ./coverage/lcov.info | ./node_modules/.bin/coveralls || true; fi' 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | 4 | ## 0.2.1 pre-release (2016-04-30) 5 | 6 | * Add Node.js 6.x support 7 | 8 | ## 0.2.0 pre-release (2016-04-16) 9 | 10 | * Add Node.js 5.x support 11 | * Update dependencies 12 | * redis: `~1.0` to `^2` 13 | 14 | ## 0.1.2 pre-release (2015-09-17) 15 | 16 | * Add Node.js 4.x support 17 | * Add test coverage reporting 18 | * Update dependencies 19 | 20 | ## 0.1.1 pre-release (2015-06-21) 21 | 22 | * Update dependencies 23 | * Use JSCS to check for code-style issues 24 | 25 | ## 0.1.0 pre-release (2015-05-03) 26 | 27 | * Initial release 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2015, Rowan Manning 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include Makefile.node 2 | -------------------------------------------------------------------------------- /Makefile.node: -------------------------------------------------------------------------------- 1 | # 2 | # Node.js Makefile 3 | # ================ 4 | # 5 | # Do not update this file manually – it's maintained separately on GitHub: 6 | # https://github.com/rowanmanning/makefiles/blob/master/Makefile.node 7 | # 8 | # To update to the latest version, run `make update-makefile`. 9 | # 10 | 11 | 12 | # Meta tasks 13 | # ---------- 14 | 15 | .PHONY: test 16 | 17 | 18 | # Useful variables 19 | # ---------------- 20 | 21 | NPM_BIN = ./node_modules/.bin 22 | export PATH := $(NPM_BIN):$(PATH) 23 | export EXPECTED_COVERAGE := 90 24 | export INTEGRATION_TIMEOUT := 5000 25 | export INTEGRATION_SLOW := 4000 26 | 27 | 28 | # Output helpers 29 | # -------------- 30 | 31 | TASK_DONE = echo "✓ $@ done" 32 | 33 | 34 | # Group tasks 35 | # ----------- 36 | 37 | all: install ci 38 | ci: verify test 39 | 40 | 41 | # Install tasks 42 | # ------------- 43 | 44 | clean: 45 | @git clean -fxd 46 | @$(TASK_DONE) 47 | 48 | install: node_modules 49 | @$(TASK_DONE) 50 | 51 | node_modules: package.json 52 | @npm prune --production=false 53 | @npm install 54 | @$(TASK_DONE) 55 | 56 | 57 | # Verify tasks 58 | # ------------ 59 | 60 | verify: verify-javascript verify-dust verify-spaces 61 | @$(TASK_DONE) 62 | 63 | verify-javascript: verify-eslint verify-jshint verify-jscs 64 | @$(TASK_DONE) 65 | 66 | verify-dust: 67 | @if [ -e .dustmiterc ]; then dustmite --path ./view && $(TASK_DONE); fi 68 | 69 | verify-eslint: 70 | @if [ -e .eslintrc ]; then eslint . && $(TASK_DONE); fi 71 | 72 | verify-jshint: 73 | @if [ -e .jshintrc ]; then jshint . && $(TASK_DONE); fi 74 | 75 | verify-jscs: 76 | @if [ -e .jscsrc ]; then jscs . && $(TASK_DONE); fi 77 | 78 | verify-spaces: 79 | @if [ -e .editorconfig ] && [ -x $(NPM_BIN)/lintspaces ]; then \ 80 | git ls-files | xargs lintspaces -e .editorconfig && $(TASK_DONE); \ 81 | fi 82 | 83 | verify-coverage: 84 | @if [ -d coverage/lcov-report ] && [ -x $(NPM_BIN)/istanbul ]; then \ 85 | istanbul check-coverage --statement $(EXPECTED_COVERAGE) --branch $(EXPECTED_COVERAGE) --function $(EXPECTED_COVERAGE) && $(TASK_DONE); \ 86 | fi 87 | 88 | # Test tasks 89 | # ---------- 90 | 91 | test: test-unit-coverage verify-coverage test-integration 92 | @$(TASK_DONE) 93 | 94 | test-unit: 95 | @if [ -d test/unit ]; then mocha test/unit --recursive && $(TASK_DONE); fi 96 | 97 | test-unit-coverage: 98 | @if [ -d test/unit ]; then \ 99 | if [ -x $(NPM_BIN)/istanbul ]; then \ 100 | istanbul cover $(NPM_BIN)/_mocha -- test/unit --recursive && $(TASK_DONE); \ 101 | else \ 102 | make test-unit; \ 103 | fi \ 104 | fi 105 | 106 | test-integration: 107 | @if [ -d test/integration ]; then mocha test/integration --timeout $(INTEGRATION_TIMEOUT) --slow $(INTEGRATION_SLOW) && $(TASK_DONE); fi 108 | 109 | 110 | # Tooling tasks 111 | # ------------- 112 | 113 | update-makefile: 114 | @curl -s https://raw.githubusercontent.com/rowanmanning/makefiles/master/Makefile.node > Makefile.node 115 | @$(TASK_DONE) 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Pacer 3 | ===== 4 | 5 | A flexible, fault-tolerant, Redis-based rate-limiter for Node.js. 6 | 7 | **⚠️ NOTE: This project is no longer being maintained. If you're interested in taking over maintenance, please contact me.** 8 | 9 | [![NPM version][shield-npm]][info-npm] 10 | [![Node.js version support][shield-node]][info-node] 11 | [![Build status][shield-build]][info-build] 12 | [![Code coverage][shield-coverage]][info-coverage] 13 | [![Dependencies][shield-dependencies]][info-dependencies] 14 | [![MIT licensed][shield-license]][info-license] 15 | 16 | ```js 17 | var createPacer = require('pacer'); 18 | 19 | var pacer = createPacer({ 20 | limit: 5, // Allow 5 requests... 21 | reset: 10 // ...every 10 seconds 22 | }); 23 | 24 | pacer.consume('consumer-identifier', function (consumer) { 25 | // consumer == { 26 | // id: 'consumer-identifier', 27 | // limit: 5, 28 | // remaining: 4, 29 | // reset: 10, 30 | // allowed: true 31 | // } 32 | }); 33 | ``` 34 | 35 | Table Of Contents 36 | ----------------- 37 | 38 | - [Install](#install) 39 | - [Getting Started](#getting-started) 40 | - [Usage](#usage) 41 | - [Options](#options) 42 | - [Examples](#examples) 43 | - [Contributing](#contributing) 44 | - [License](#license) 45 | 46 | 47 | Install 48 | ------- 49 | 50 | Install Pacer with [npm][npm]: 51 | 52 | ```sh 53 | npm install pacer 54 | ``` 55 | 56 | 57 | Getting Started 58 | --------------- 59 | 60 | Require in and Pacer: 61 | 62 | ```js 63 | var createPacer = require('pacer'); 64 | ``` 65 | 66 | Create a Pacer instance, passing in some [options](#options): 67 | 68 | ```js 69 | var pacer = createPacer({ 70 | limit: 5, // Allow 5 requests... 71 | reset: 10 // ...every 10 seconds 72 | }); 73 | ``` 74 | 75 | Consume a token for the `'foo'` consumer. The consumer identifier can be any string you like, for example the request IP, or a username. 76 | 77 | ```js 78 | pacer.consume('foo', function (consumer) { 79 | // consumer == { 80 | // id: 'foo', 81 | // limit: 5, 82 | // remaining: 4, 83 | // reset: 10, 84 | // allowed: true 85 | // } 86 | }); 87 | ``` 88 | 89 | Consumer can also be an object, allowing you to specify a custom limit and reset time for them. This could be used to provide different tiers of rate limiting depending on the user that is accessing your application. 90 | 91 | ```js 92 | pacer.consume({ 93 | id: 'foo', 94 | limit: 10, // Allow 10 requests... 95 | reset: 5 // ...every 5 seconds 96 | }, function (consumer) { 97 | // consumer == { 98 | // id: 'consumer-identifier', 99 | // limit: 10, 100 | // remaining: 9, 101 | // reset: 5, 102 | // allowed: true 103 | // } 104 | }); 105 | ``` 106 | 107 | If you don't wish to consume a token but want to query how many tokens a consumer has remaining, you can use the `query` method. This works with string or object consumers. 108 | 109 | ```js 110 | pacer.query('foo', function (consumer) { 111 | // consumer == { 112 | // id: 'foo', 113 | // limit: 5, 114 | // remaining: 5, 115 | // reset: 10, 116 | // allowed: true 117 | // } 118 | }); 119 | ``` 120 | 121 | 122 | Usage 123 | ----- 124 | 125 | Create a pacer with the passed in [`options`](#options) object: 126 | 127 | ```js 128 | var pacer = createPacer({ /* ... */ }); 129 | ``` 130 | 131 | ### Consuming 132 | 133 | #### `pacer.consume(consumerId, callback)` 134 | 135 | Consume a token for a specified consumer. 136 | 137 | ```js 138 | consumerId = String 139 | callback = function (consumer) { 140 | consumer = { 141 | id: String, 142 | limit: Number, 143 | remaining: Number, 144 | reset: Number, 145 | allowed: Boolean 146 | } 147 | } 148 | ``` 149 | 150 | #### `pacer.consume(consumer, callback)` 151 | 152 | Consume a token for a specified consumer using a custom limit and reset time. 153 | 154 | ```js 155 | consumer = { 156 | id: String, 157 | limit: Number, 158 | reset: Number 159 | } 160 | callback = function (consumer) { 161 | consumer = { 162 | id: String, 163 | limit: Number, 164 | remaining: Number, 165 | reset: Number, 166 | allowed: Boolean 167 | } 168 | } 169 | ``` 170 | 171 | ### Querying 172 | 173 | #### `pacer.query(consumerId, callback)` 174 | 175 | Query the tokens for a specified consumer. 176 | 177 | ```js 178 | consumerId = String 179 | callback = function (consumer) { 180 | consumer = { 181 | id: String, 182 | limit: Number, 183 | remaining: Number, 184 | reset: Number, 185 | allowed: Boolean 186 | } 187 | } 188 | ``` 189 | 190 | #### `pacer.query(consumer, callback)` 191 | 192 | Query the tokens for a specified consumer using a custom limit and reset time. 193 | 194 | ```js 195 | consumer = { 196 | id: String, 197 | limit: Number, 198 | reset: Number 199 | } 200 | callback = function (consumer) { 201 | consumer = { 202 | id: String, 203 | limit: Number, 204 | remaining: Number, 205 | reset: Number, 206 | allowed: Boolean 207 | } 208 | } 209 | ``` 210 | 211 | 212 | Options 213 | ------- 214 | 215 | #### `allowOnError` (boolean) 216 | 217 | Whether to allow access for all consumers if the Redis server errors or is down. Defaults to `true`. 218 | 219 | #### `limit` (number) 220 | 221 | The maximum number of tokens a consumer can use. Defaults to `100`. 222 | 223 | #### `redisHost` (string) 224 | 225 | The host Redis is running on. Defaults to `'localhost'`. 226 | 227 | #### `redisIndex` (number) 228 | 229 | The Redis database index to use. Defaults to `0`. 230 | 231 | #### `redisPort` (number) 232 | 233 | The port Redis is running on. Defaults to `6379`. 234 | 235 | #### `reset` (number) 236 | 237 | The amount of time (in seconds) before tokens for a consumer are reset to their maximum. Defaults to `3600`. 238 | 239 | 240 | Examples 241 | -------- 242 | 243 | Pacer comes with a few examples which demonstrate how you can integrate it with your applications: 244 | 245 | #### Basic Example 246 | 247 | A simple command-line interface which consumes a rate-limiting token for the given consumer every time it's called. 248 | 249 | ``` 250 | node example/basic myconsumerid 251 | ``` 252 | 253 | #### Query Example 254 | 255 | A simple command-line interface which queries the rate-limiting tokens for the given consumer every time it's called, without consuming one. 256 | 257 | ``` 258 | node example/query myconsumerid 259 | ``` 260 | 261 | #### Basic Connect Example 262 | 263 | A connect application which consumes a rate-limiting token for the client IP whenever a request it made. 264 | 265 | ``` 266 | node example/connect-basic 267 | ``` 268 | 269 | Then open http://localhost:3000/ in your browser. 270 | 271 | #### Connect With Varying Rates Example 272 | 273 | A connect application which consumes a rate-limiting token for the client IP whenever a request it made. Google Chrome users get a higher rate limit and a faster reset time, to demonstrate varying rates for different consumer types. 274 | 275 | ``` 276 | node example/connect-varying-rates 277 | ``` 278 | 279 | Then open http://localhost:3000/ in your browser. 280 | 281 | 282 | Contributing 283 | ------------ 284 | 285 | To contribute to Pacer, clone this repo locally and commit your code on a separate branch. 286 | 287 | Please write unit tests for your code, and check that everything works by running the following before opening a pull-request: 288 | 289 | ```sh 290 | make ci 291 | ``` 292 | 293 | 294 | License 295 | ------- 296 | 297 | Pacer is licensed under the [MIT][info-license] license. 298 | Copyright © 2015, Rowan Manning 299 | 300 | 301 | 302 | [npm]: https://npmjs.org/ 303 | 304 | [info-coverage]: https://coveralls.io/github/rowanmanning/pacer 305 | [info-dependencies]: https://gemnasium.com/rowanmanning/pacer 306 | [info-license]: LICENSE 307 | [info-node]: package.json 308 | [info-npm]: https://www.npmjs.com/package/pacer 309 | [info-build]: https://travis-ci.org/rowanmanning/pacer 310 | [shield-coverage]: https://img.shields.io/coveralls/rowanmanning/pacer.svg 311 | [shield-dependencies]: https://img.shields.io/gemnasium/rowanmanning/pacer.svg 312 | [shield-license]: https://img.shields.io/badge/license-MIT-blue.svg 313 | [shield-node]: https://img.shields.io/badge/node.js%20support-0.10–7-brightgreen.svg 314 | [shield-npm]: https://img.shields.io/npm/v/pacer.svg 315 | [shield-build]: https://img.shields.io/travis/rowanmanning/pacer/master.svg 316 | -------------------------------------------------------------------------------- /example/basic/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var createPacer = require('../..'); 4 | 5 | // Create a pacer 6 | var pacer = createPacer({ 7 | limit: 5, // Allow 5 requests... 8 | reset: 10 // ...every 10 seconds 9 | }); 10 | 11 | // Require an argument 12 | if (!process.argv[2]) { 13 | console.log('Please send an argument to identify the consumer'); 14 | process.exit(1); 15 | } 16 | 17 | // Consume a rate-limit token for the specified consumer 18 | pacer.consume(process.argv[2], function (consumer) { 19 | // Output the rate-limiting details 20 | console.log(consumer); 21 | process.exit(); 22 | }); 23 | -------------------------------------------------------------------------------- /example/connect-basic/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var connect = require('connect'); 4 | var createPacer = require('../..'); 5 | 6 | // Create a connect application 7 | var app = connect(); 8 | 9 | // Create a pacer 10 | var pacer = createPacer({ 11 | limit: 5, // Allow 5 requests... 12 | reset: 10 // ...every 10 seconds 13 | }); 14 | 15 | // Add a request handler 16 | app.use(function (request, response) { 17 | 18 | // We're only interested in traffic to / for this example 19 | if (request.url !== '/') { 20 | response.writeHead(404); 21 | return response.end('Not Found'); 22 | } 23 | 24 | // Consume a rate-limit token for the request IP 25 | pacer.consume(request.connection.remoteAddress, function (consumer) { 26 | // Output the rate-limiting details 27 | response.end(JSON.stringify(consumer, null, 4)); 28 | }); 29 | 30 | }); 31 | 32 | // Listen on a port 33 | var port = process.env.PORT || 3000; 34 | app.listen(port, function () { 35 | console.log('Application running on port %s', port); 36 | console.log('Visit http://localhost:%s/ in your browser', port); 37 | }); 38 | -------------------------------------------------------------------------------- /example/connect-varying-rates/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var connect = require('connect'); 4 | var createPacer = require('../..'); 5 | 6 | // Create a connect application 7 | var app = connect(); 8 | 9 | // Create a pacer 10 | var pacer = createPacer({ 11 | limit: 5, // Allow 5 requests... 12 | reset: 10 // ...every 10 seconds 13 | }); 14 | 15 | // Add a request handler 16 | app.use(function (request, response) { 17 | 18 | // We're only interested in traffic to / for this example 19 | if (request.url !== '/') { 20 | response.writeHead(404); 21 | return response.end('Not Found'); 22 | } 23 | 24 | // Create consumer information 25 | var consumer = { 26 | id: request.connection.remoteAddress + ', ' + request.headers['user-agent'] 27 | }; 28 | 29 | // Give Google Chrome users a better rate limit and reset time 30 | if (request.headers['user-agent'].indexOf('Chrome/') !== -1) { 31 | consumer.limit = 10; // Allow 10 requests... 32 | consumer.reset = 5; // ...every 5 seconds 33 | } 34 | 35 | // Consume a rate-limit token for the consumer 36 | pacer.consume(consumer, function (consumer) { 37 | // Output the rate-limiting details 38 | response.end(JSON.stringify(consumer, null, 4)); 39 | }); 40 | 41 | }); 42 | 43 | // Listen on a port 44 | var port = process.env.PORT || 3000; 45 | app.listen(port, function () { 46 | console.log('Application running on port %s', port); 47 | console.log('Visit http://localhost:%s/ in your browser', port); 48 | }); 49 | -------------------------------------------------------------------------------- /example/query/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var createPacer = require('../..'); 4 | 5 | // Create a pacer 6 | var pacer = createPacer({ 7 | limit: 5, // Allow 5 requests... 8 | reset: 10 // ...every 10 seconds 9 | }); 10 | 11 | // Require an argument 12 | if (!process.argv[2]) { 13 | console.log('Please send an argument to identify the consumer'); 14 | process.exit(1); 15 | } 16 | 17 | // Query the rate-limit for the specified consumer without consuming a token 18 | pacer.query(process.argv[2], function (consumer) { 19 | // Output the rate-limiting details 20 | console.log(consumer); 21 | process.exit(); 22 | }); 23 | -------------------------------------------------------------------------------- /lib/pacer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'); 4 | var redis = require('redis'); 5 | 6 | module.exports = createPacer; 7 | module.exports.defaults = { 8 | allowOnError: true, 9 | redisHost: 'localhost', 10 | redisPort: 6379, 11 | redisIndex: 0, 12 | limit: 100, 13 | reset: 3600 14 | }; 15 | 16 | function createPacer (options) { 17 | options = defaultOptions(options); 18 | var pacer = { 19 | database: createRedisClient( 20 | options.redisHost, 21 | options.redisPort, 22 | options.redisIndex 23 | ), 24 | limit: options.limit, 25 | reset: options.reset, 26 | allowOnError: Boolean(options.allowOnError) 27 | }; 28 | return { 29 | consume: consumeRateLimit.bind(null, pacer), 30 | query: queryRateLimit.bind(null, pacer) 31 | }; 32 | } 33 | 34 | function createRedisClient (host, port, database) { 35 | var client = redis.createClient(port, host); 36 | client.select(database); 37 | return client; 38 | } 39 | 40 | function consumeRateLimit (pacer, consumer, done) { 41 | performQuery(pacer, consumer, true, done); 42 | } 43 | 44 | function queryRateLimit (pacer, consumer, done) { 45 | performQuery(pacer, consumer, false, done); 46 | } 47 | 48 | function performQuery (pacer, consumer, shouldConsume, done) { 49 | consumer = resolveConsumer(consumer, pacer); 50 | var multi = pacer.database.multi(); 51 | multi.set([consumer.id, consumer.limit, 'NX', 'EX', consumer.reset]); 52 | if (shouldConsume) { 53 | multi.decr(consumer.id); 54 | } 55 | else { 56 | multi.get(consumer.id); 57 | } 58 | multi.ttl(consumer.id); 59 | multi.exec(function (error, results) { 60 | if (error || !results) { 61 | consumer.remaining = (pacer.allowOnError ? consumer.limit : 0); 62 | consumer.reset = 0; 63 | } 64 | else { 65 | consumer.remaining = Math.max(0, parseInt(results[1], 10)); 66 | consumer.reset = parseInt(results[2], 10); 67 | } 68 | consumer.error = error; 69 | consumer.allowed = (consumer.remaining > 0); 70 | done(consumer); 71 | }); 72 | } 73 | 74 | function resolveConsumer (consumer, pacer) { 75 | return { 76 | id: resolveConsumerId(consumer), 77 | limit: parseInt(resolveConsumerLimit(consumer, pacer), 10), 78 | reset: parseInt(resolveConsumerReset(consumer, pacer), 10) 79 | }; 80 | } 81 | 82 | function resolveConsumerId (consumer) { 83 | if (isObject(consumer)) { 84 | return consumer.id; 85 | } 86 | return String(consumer); 87 | } 88 | 89 | function resolveConsumerLimit (consumer, pacer) { 90 | if (isObject(consumer)) { 91 | return consumer.limit || pacer.limit; 92 | } 93 | return pacer.limit; 94 | } 95 | 96 | function resolveConsumerReset (consumer, pacer) { 97 | if (isObject(consumer)) { 98 | return consumer.reset || pacer.reset; 99 | } 100 | return pacer.reset; 101 | } 102 | 103 | function defaultOptions (options) { 104 | return _.defaults({}, options, createPacer.defaults); 105 | } 106 | 107 | function isObject (value) { 108 | return (value !== null && !Array.isArray(value) && typeof value === 'object'); 109 | } 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pacer", 3 | "version": "0.2.2", 4 | "description": "A flexible, fault-tolerant, Redis-based rate-limiter", 5 | "keywords": [ 6 | "limit", 7 | "pace", 8 | "rate" 9 | ], 10 | "author": "Rowan Manning (http://rowanmanning.com/)", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/rowanmanning/pacer.git" 14 | }, 15 | "homepage": "https://github.com/rowanmanning/pacer", 16 | "bugs": "https://github.com/rowanmanning/pacer/issues", 17 | "license": "MIT", 18 | "engines": { 19 | "node": ">=0.10" 20 | }, 21 | "dependencies": { 22 | "redis": "^2", 23 | "underscore": "~1.8" 24 | }, 25 | "devDependencies": { 26 | "connect": "^3", 27 | "istanbul": "^0.4", 28 | "jscs": "^2", 29 | "jshint": "^2", 30 | "mocha": "^2", 31 | "mockery": "~1.4", 32 | "proclaim": "^3", 33 | "sinon": "^1" 34 | }, 35 | "main": "./lib/pacer.js", 36 | "scripts": { 37 | "test": "make ci" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/unit/lib/pacer.js: -------------------------------------------------------------------------------- 1 | // jshint maxstatements: false 2 | // jscs:disable disallowMultipleVarDecl, maximumLineLength 3 | 'use strict'; 4 | 5 | var assert = require('proclaim'); 6 | var mockery = require('mockery'); 7 | 8 | describe('lib/pacer', function () { 9 | var createPacer, redis, underscore; 10 | 11 | beforeEach(function () { 12 | 13 | redis = require('../mock/redis'); 14 | mockery.registerMock('redis', redis); 15 | 16 | underscore = require('../mock/underscore'); 17 | mockery.registerMock('underscore', underscore); 18 | 19 | createPacer = require('../../../lib/pacer'); 20 | 21 | }); 22 | 23 | it('should be a function', function () { 24 | assert.isFunction(createPacer); 25 | }); 26 | 27 | it('should have a `defaults` property', function () { 28 | assert.isObject(createPacer.defaults); 29 | }); 30 | 31 | describe('.defaults', function () { 32 | var defaults; 33 | 34 | beforeEach(function () { 35 | defaults = createPacer.defaults; 36 | }); 37 | 38 | it('should have an `allowOnError` property', function () { 39 | assert.isTrue(defaults.allowOnError); 40 | }); 41 | 42 | it('should have a `redisHost` property', function () { 43 | assert.strictEqual(defaults.redisHost, 'localhost'); 44 | }); 45 | 46 | it('should have a `redisPort` property', function () { 47 | assert.strictEqual(defaults.redisPort, 6379); 48 | }); 49 | 50 | it('should have a `redisIndex` property', function () { 51 | assert.strictEqual(defaults.redisIndex, 0); 52 | }); 53 | 54 | it('should have a `limit` property', function () { 55 | assert.strictEqual(defaults.limit, 100); 56 | }); 57 | 58 | it('should have a `reset` property', function () { 59 | assert.strictEqual(defaults.reset, 3600); 60 | }); 61 | 62 | }); 63 | 64 | describe('createPacer()', function () { 65 | var options, pacer, redisClient; 66 | 67 | beforeEach(function () { 68 | options = { 69 | allowOnError: true, 70 | redisHost: 'localhost', 71 | redisPort: 6379, 72 | redisIndex: 0, 73 | limit: 100, 74 | reset: 3600 75 | }; 76 | pacer = createPacer(options); 77 | redisClient = redis.createClient.firstCall.returnValue; 78 | }); 79 | 80 | it('should default the options', function () { 81 | assert.isTrue(underscore.defaults.calledOnce); 82 | assert.deepEqual(underscore.defaults.firstCall.args[0], {}); 83 | assert.strictEqual(underscore.defaults.firstCall.args[1], options); 84 | assert.strictEqual(underscore.defaults.firstCall.args[2], createPacer.defaults); 85 | }); 86 | 87 | it('should create a Redis client', function () { 88 | assert.isTrue(redis.createClient.withArgs(options.redisPort, options.redisHost).calledOnce); 89 | }); 90 | 91 | it('should select a Redis database', function () { 92 | assert.isTrue(redisClient.select.withArgs(options.redisIndex).calledOnce); 93 | }); 94 | 95 | it('should return an object', function () { 96 | assert.isObject(pacer); 97 | }); 98 | 99 | describe('returned object', function () { 100 | 101 | it('should have a `consume` method', function () { 102 | assert.isFunction(pacer.consume); 103 | }); 104 | 105 | describe('.consume() with a string consumer', function () { 106 | var consumer, multi; 107 | 108 | beforeEach(function (done) { 109 | consumer = 'foo'; 110 | pacer.consume(consumer, function () { 111 | multi = redisClient.multi.firstCall.returnValue; 112 | done(); 113 | }); 114 | }); 115 | 116 | it('should create a Redis multi command', function () { 117 | assert.isTrue(redisClient.multi.calledOnce); 118 | }); 119 | 120 | it('should SET the consumer key in Redis', function () { 121 | assert.isTrue(multi.set.calledOnce); 122 | assert.deepEqual(multi.set.firstCall.args[0], [ 123 | consumer, 124 | options.limit, 125 | 'NX', 126 | 'EX', 127 | options.reset 128 | ]); 129 | }); 130 | 131 | it('should DECR the consumer key in Redis', function () { 132 | assert.isTrue(multi.decr.withArgs(consumer).calledOnce); 133 | }); 134 | 135 | it('should not GET the consumer key in Redis', function () { 136 | assert.isFalse(multi.get.called); 137 | }); 138 | 139 | it('should get the TTL of the consumer key in Redis', function () { 140 | assert.isTrue(multi.ttl.withArgs(consumer).calledOnce); 141 | }); 142 | 143 | it('should execute the multi command with a callback', function () { 144 | assert.isTrue(multi.exec.calledOnce); 145 | assert.isFunction(multi.exec.firstCall.args[0]); 146 | }); 147 | 148 | it('should call Redis multi functions in order', function () { 149 | assert.callOrder( 150 | redisClient.multi, 151 | multi.set, 152 | multi.decr, 153 | multi.ttl, 154 | multi.exec 155 | ); 156 | }); 157 | 158 | }); 159 | 160 | describe('.consume() with an object consumer', function () { 161 | var consumer, multi; 162 | 163 | beforeEach(function (done) { 164 | consumer = { 165 | id: 'foo', 166 | limit: 1234, 167 | reset: 5678 168 | }; 169 | pacer.consume(consumer, function () { 170 | multi = redisClient.multi.firstCall.returnValue; 171 | done(); 172 | }); 173 | }); 174 | 175 | it('should create a Redis multi command', function () { 176 | assert.isTrue(redisClient.multi.calledOnce); 177 | }); 178 | 179 | it('should SET the consumer key in Redis', function () { 180 | assert.isTrue(multi.set.calledOnce); 181 | assert.deepEqual(multi.set.firstCall.args[0], [ 182 | consumer.id, 183 | consumer.limit, 184 | 'NX', 185 | 'EX', 186 | consumer.reset 187 | ]); 188 | }); 189 | 190 | it('should DECR the consumer key in Redis', function () { 191 | assert.isTrue(multi.decr.withArgs(consumer.id).calledOnce); 192 | }); 193 | 194 | it('should not GET the consumer key in Redis', function () { 195 | assert.isFalse(multi.get.called); 196 | }); 197 | 198 | it('should get the TTL of the consumer key in Redis', function () { 199 | assert.isTrue(multi.ttl.withArgs(consumer.id).calledOnce); 200 | }); 201 | 202 | it('should execute the multi command with a callback', function () { 203 | assert.isTrue(multi.exec.calledOnce); 204 | assert.isFunction(multi.exec.firstCall.args[0]); 205 | }); 206 | 207 | it('should call Redis multi functions in order', function () { 208 | assert.callOrder( 209 | redisClient.multi, 210 | multi.set, 211 | multi.decr, 212 | multi.ttl, 213 | multi.exec 214 | ); 215 | }); 216 | 217 | }); 218 | 219 | describe('.consume() with an object consumer that has no defined limit', function () { 220 | var consumer, multi; 221 | 222 | beforeEach(function (done) { 223 | consumer = { 224 | id: 'foo', 225 | reset: 5678 226 | }; 227 | pacer.consume(consumer, function () { 228 | multi = redisClient.multi.firstCall.returnValue; 229 | done(); 230 | }); 231 | }); 232 | 233 | it('should SET the consumer key in Redis with the default limit', function () { 234 | assert.isTrue(multi.set.calledOnce); 235 | assert.strictEqual(multi.set.firstCall.args[0][1], options.limit); 236 | }); 237 | 238 | }); 239 | 240 | describe('.consume() with an object consumer that has no defined reset', function () { 241 | var consumer, multi; 242 | 243 | beforeEach(function (done) { 244 | consumer = { 245 | id: 'foo', 246 | limit: 1234 247 | }; 248 | pacer.consume(consumer, function () { 249 | multi = redisClient.multi.firstCall.returnValue; 250 | done(); 251 | }); 252 | }); 253 | 254 | it('should SET the consumer key in Redis with the default reset', function () { 255 | assert.isTrue(multi.set.calledOnce); 256 | assert.strictEqual(multi.set.firstCall.args[0][4], options.reset); 257 | }); 258 | 259 | }); 260 | 261 | describe('.consume() result handler', function () { 262 | var exec; 263 | 264 | beforeEach(function () { 265 | exec = redisClient.multi.defaultBehavior.returnValue.exec; 266 | }); 267 | 268 | describe('when there are tokens remaining', function () { 269 | var consumer, resultHandler; 270 | 271 | beforeEach(function (done) { 272 | exec.yields(null, [ 273 | null, 274 | '12', // remaining 275 | '345' // reset 276 | ]); 277 | pacer.consume('foo', function (result) { 278 | consumer = result; 279 | resultHandler = exec.firstCall.args[0]; 280 | done(); 281 | }); 282 | }); 283 | 284 | it('should callback with the expected consumer object', function () { 285 | assert.strictEqual(consumer.id, 'foo'); 286 | assert.strictEqual(consumer.limit, 100); 287 | assert.strictEqual(consumer.reset, 345); 288 | assert.strictEqual(consumer.remaining, 12); 289 | assert.isNull(consumer.error); 290 | assert.isTrue(consumer.allowed); 291 | }); 292 | 293 | }); 294 | 295 | describe('when no more tokens are remaining', function () { 296 | var consumer, resultHandler; 297 | 298 | beforeEach(function (done) { 299 | exec.yields(null, [ 300 | null, 301 | '0' // remaining 302 | ]); 303 | pacer.consume('foo', function (result) { 304 | consumer = result; 305 | resultHandler = exec.firstCall.args[0]; 306 | done(); 307 | }); 308 | }); 309 | 310 | it('should callback with the expected consumer object', function () { 311 | assert.strictEqual(consumer.remaining, 0); 312 | assert.isNull(consumer.error); 313 | assert.isFalse(consumer.allowed); 314 | }); 315 | 316 | }); 317 | 318 | describe('when remaining tokens are negative', function () { 319 | var consumer, resultHandler; 320 | 321 | beforeEach(function (done) { 322 | exec.yields(null, [ 323 | null, 324 | '-1' // remaining 325 | ]); 326 | pacer.consume('foo', function (result) { 327 | consumer = result; 328 | resultHandler = exec.firstCall.args[0]; 329 | done(); 330 | }); 331 | }); 332 | 333 | it('should callback with the expected consumer object', function () { 334 | assert.strictEqual(consumer.remaining, 0); 335 | assert.isNull(consumer.error); 336 | assert.isFalse(consumer.allowed); 337 | }); 338 | 339 | }); 340 | 341 | describe('when the Redis query errors and `options.allowOnError` is `true`', function () { 342 | var error, consumer, resultHandler; 343 | 344 | beforeEach(function (done) { 345 | error = new Error('...'); 346 | exec.yields(error, null); 347 | pacer.consume('foo', function (result) { 348 | consumer = result; 349 | resultHandler = exec.firstCall.args[0]; 350 | done(); 351 | }); 352 | }); 353 | 354 | it('should callback with the expected consumer object', function () { 355 | assert.strictEqual(consumer.id, 'foo'); 356 | assert.strictEqual(consumer.limit, 100); 357 | assert.strictEqual(consumer.reset, 0); 358 | assert.strictEqual(consumer.remaining, 100); 359 | assert.strictEqual(consumer.error, error); 360 | assert.isTrue(consumer.allowed); 361 | }); 362 | 363 | }); 364 | 365 | describe('when the Redis query errors and `options.allowOnError` is `false`', function () { 366 | var error, consumer, resultHandler; 367 | 368 | beforeEach(function (done) { 369 | error = new Error('...'); 370 | exec.yields(error, null); 371 | options.allowOnError = false; 372 | pacer = createPacer(options); 373 | pacer.consume('foo', function (result) { 374 | consumer = result; 375 | resultHandler = exec.firstCall.args[0]; 376 | done(); 377 | }); 378 | }); 379 | 380 | it('should callback with the expected consumer object', function () { 381 | assert.strictEqual(consumer.id, 'foo'); 382 | assert.strictEqual(consumer.limit, 100); 383 | assert.strictEqual(consumer.reset, 0); 384 | assert.strictEqual(consumer.remaining, 0); 385 | assert.strictEqual(consumer.error, error); 386 | assert.isFalse(consumer.allowed); 387 | }); 388 | 389 | }); 390 | 391 | }); 392 | 393 | it('should have a `query` method', function () { 394 | assert.isFunction(pacer.query); 395 | }); 396 | 397 | describe('.query() with a string consumer', function () { 398 | var consumer, multi; 399 | 400 | beforeEach(function (done) { 401 | consumer = 'foo'; 402 | pacer.query(consumer, function () { 403 | multi = redisClient.multi.firstCall.returnValue; 404 | done(); 405 | }); 406 | }); 407 | 408 | it('should create a Redis multi command', function () { 409 | assert.isTrue(redisClient.multi.calledOnce); 410 | }); 411 | 412 | it('should SET the consumer key in Redis', function () { 413 | assert.isTrue(multi.set.calledOnce); 414 | assert.deepEqual(multi.set.firstCall.args[0], [ 415 | consumer, 416 | options.limit, 417 | 'NX', 418 | 'EX', 419 | options.reset 420 | ]); 421 | }); 422 | 423 | it('should GET the consumer key in Redis', function () { 424 | assert.isTrue(multi.get.withArgs(consumer).calledOnce); 425 | }); 426 | 427 | it('should not DECR the consumer key in Redis', function () { 428 | assert.isFalse(multi.decr.called); 429 | }); 430 | 431 | it('should get the TTL of the consumer key in Redis', function () { 432 | assert.isTrue(multi.ttl.withArgs(consumer).calledOnce); 433 | }); 434 | 435 | it('should execute the multi command with a callback', function () { 436 | assert.isTrue(multi.exec.calledOnce); 437 | assert.isFunction(multi.exec.firstCall.args[0]); 438 | }); 439 | 440 | it('should call Redis multi functions in order', function () { 441 | assert.callOrder( 442 | redisClient.multi, 443 | multi.set, 444 | multi.get, 445 | multi.ttl, 446 | multi.exec 447 | ); 448 | }); 449 | 450 | }); 451 | 452 | describe('.query() with an object consumer', function () { 453 | var consumer, multi; 454 | 455 | beforeEach(function (done) { 456 | consumer = { 457 | id: 'foo', 458 | limit: 1234, 459 | reset: 5678 460 | }; 461 | pacer.query(consumer, function () { 462 | multi = redisClient.multi.firstCall.returnValue; 463 | done(); 464 | }); 465 | }); 466 | 467 | it('should create a Redis multi command', function () { 468 | assert.isTrue(redisClient.multi.calledOnce); 469 | }); 470 | 471 | it('should SET the consumer key in Redis', function () { 472 | assert.isTrue(multi.set.calledOnce); 473 | assert.deepEqual(multi.set.firstCall.args[0], [ 474 | consumer.id, 475 | consumer.limit, 476 | 'NX', 477 | 'EX', 478 | consumer.reset 479 | ]); 480 | }); 481 | 482 | it('should GET the consumer key in Redis', function () { 483 | assert.isTrue(multi.get.withArgs(consumer.id).calledOnce); 484 | }); 485 | 486 | it('should not DECR the consumer key in Redis', function () { 487 | assert.isFalse(multi.decr.called); 488 | }); 489 | 490 | it('should get the TTL of the consumer key in Redis', function () { 491 | assert.isTrue(multi.ttl.withArgs(consumer.id).calledOnce); 492 | }); 493 | 494 | it('should execute the multi command with a callback', function () { 495 | assert.isTrue(multi.exec.calledOnce); 496 | assert.isFunction(multi.exec.firstCall.args[0]); 497 | }); 498 | 499 | it('should call Redis multi functions in order', function () { 500 | assert.callOrder( 501 | redisClient.multi, 502 | multi.set, 503 | multi.get, 504 | multi.ttl, 505 | multi.exec 506 | ); 507 | }); 508 | 509 | }); 510 | 511 | describe('.query() result handler', function () { 512 | var exec; 513 | 514 | beforeEach(function () { 515 | exec = redisClient.multi.defaultBehavior.returnValue.exec; 516 | }); 517 | 518 | describe('when there are tokens remaining', function () { 519 | var consumer, resultHandler; 520 | 521 | beforeEach(function (done) { 522 | exec.yields(null, [ 523 | null, 524 | '12', // remaining 525 | '345' // reset 526 | ]); 527 | pacer.query('foo', function (result) { 528 | consumer = result; 529 | resultHandler = exec.firstCall.args[0]; 530 | done(); 531 | }); 532 | }); 533 | 534 | it('should callback with the expected consumer object', function () { 535 | assert.strictEqual(consumer.id, 'foo'); 536 | assert.strictEqual(consumer.limit, 100); 537 | assert.strictEqual(consumer.reset, 345); 538 | assert.strictEqual(consumer.remaining, 12); 539 | assert.isNull(consumer.error); 540 | assert.isTrue(consumer.allowed); 541 | }); 542 | 543 | }); 544 | 545 | describe('when no more tokens are remaining', function () { 546 | var consumer, resultHandler; 547 | 548 | beforeEach(function (done) { 549 | exec.yields(null, [ 550 | null, 551 | '0' // remaining 552 | ]); 553 | pacer.query('foo', function (result) { 554 | consumer = result; 555 | resultHandler = exec.firstCall.args[0]; 556 | done(); 557 | }); 558 | }); 559 | 560 | it('should callback with the expected consumer object', function () { 561 | assert.strictEqual(consumer.remaining, 0); 562 | assert.isNull(consumer.error); 563 | assert.isFalse(consumer.allowed); 564 | }); 565 | 566 | }); 567 | 568 | describe('when remaining tokens are negative', function () { 569 | var consumer, resultHandler; 570 | 571 | beforeEach(function (done) { 572 | exec.yields(null, [ 573 | null, 574 | '-1' // remaining 575 | ]); 576 | pacer.query('foo', function (result) { 577 | consumer = result; 578 | resultHandler = exec.firstCall.args[0]; 579 | done(); 580 | }); 581 | }); 582 | 583 | it('should callback with the expected consumer object', function () { 584 | assert.strictEqual(consumer.remaining, 0); 585 | assert.isNull(consumer.error); 586 | assert.isFalse(consumer.allowed); 587 | }); 588 | 589 | }); 590 | 591 | describe('when the Redis query errors and `options.allowOnError` is `true`', function () { 592 | var error, consumer, resultHandler; 593 | 594 | beforeEach(function (done) { 595 | error = new Error('...'); 596 | exec.yields(error, null); 597 | pacer.query('foo', function (result) { 598 | consumer = result; 599 | resultHandler = exec.firstCall.args[0]; 600 | done(); 601 | }); 602 | }); 603 | 604 | it('should callback with the expected consumer object', function () { 605 | assert.strictEqual(consumer.id, 'foo'); 606 | assert.strictEqual(consumer.limit, 100); 607 | assert.strictEqual(consumer.reset, 0); 608 | assert.strictEqual(consumer.remaining, 100); 609 | assert.strictEqual(consumer.error, error); 610 | assert.isTrue(consumer.allowed); 611 | }); 612 | 613 | }); 614 | 615 | describe('when the Redis query errors and `options.allowOnError` is `false`', function () { 616 | var error, consumer, resultHandler; 617 | 618 | beforeEach(function (done) { 619 | error = new Error('...'); 620 | exec.yields(error, null); 621 | options.allowOnError = false; 622 | pacer = createPacer(options); 623 | pacer.query('foo', function (result) { 624 | consumer = result; 625 | resultHandler = exec.firstCall.args[0]; 626 | done(); 627 | }); 628 | }); 629 | 630 | it('should callback with the expected consumer object', function () { 631 | assert.strictEqual(consumer.id, 'foo'); 632 | assert.strictEqual(consumer.limit, 100); 633 | assert.strictEqual(consumer.reset, 0); 634 | assert.strictEqual(consumer.remaining, 0); 635 | assert.strictEqual(consumer.error, error); 636 | assert.isFalse(consumer.allowed); 637 | }); 638 | 639 | }); 640 | 641 | }); 642 | 643 | }); 644 | 645 | }); 646 | 647 | }); 648 | -------------------------------------------------------------------------------- /test/unit/mock/redis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sinon = require('sinon'); 4 | 5 | module.exports = { 6 | createClient: sinon.stub().returns({ 7 | select: sinon.spy(), 8 | multi: sinon.stub().returns({ 9 | decr: sinon.spy(), 10 | exec: sinon.stub().yields(), 11 | get: sinon.spy(), 12 | set: sinon.spy(), 13 | ttl: sinon.spy() 14 | }) 15 | }) 16 | }; 17 | -------------------------------------------------------------------------------- /test/unit/mock/underscore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sinon = require('sinon'); 4 | 5 | module.exports = { 6 | defaults: sinon.stub().returnsArg(1) 7 | }; 8 | -------------------------------------------------------------------------------- /test/unit/setup.js: -------------------------------------------------------------------------------- 1 | // jshint maxstatements: false 2 | // jscs:disable disallowMultipleVarDecl, maximumLineLength 3 | 'use strict'; 4 | 5 | var assert = require('proclaim'); 6 | var mockery = require('mockery'); 7 | var sinon = require('sinon'); 8 | 9 | sinon.assert.expose(assert, { 10 | includeFail: false, 11 | prefix: '' 12 | }); 13 | 14 | beforeEach(function () { 15 | mockery.enable({ 16 | useCleanCache: true, 17 | warnOnUnregistered: false, 18 | warnOnReplace: false 19 | }); 20 | }); 21 | 22 | afterEach(function () { 23 | mockery.deregisterAll(); 24 | mockery.disable(); 25 | }); 26 | --------------------------------------------------------------------------------