├── .github └── workflows │ ├── benchmarks.yml │ └── tests.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── benchmark └── index.js ├── changelog.md ├── index.js ├── lib └── parser.js ├── package.json └── test └── parsers.spec.js /.github/workflows/benchmarks.yml: -------------------------------------------------------------------------------- 1 | name: Benchmarking 2 | 3 | on: [push] 4 | 5 | jobs: 6 | benchmark: 7 | name: Benchmark 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: [6.x, 8.x, 10.x, 12.x, 13.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | with: 17 | fetch-depth: 1 18 | 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | 24 | - run: npm i --no-audit --prefer-offline 25 | - name: Run Benchmark 26 | run: npm run benchmark 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | testing: 7 | name: Test 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: [6.x, 8.x, 10.x, 12.x, 13.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | with: 17 | fetch-depth: 1 18 | 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | 24 | - name: Install Packages 25 | run: npm i --no-audit --prefer-offline 26 | 27 | - name: Run Tests 28 | run: npm test 29 | 30 | - name: Submit Coverage 31 | run: npm run coveralls 32 | env: 33 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }} 34 | 35 | - name: Upload Coverage Report 36 | uses: actions/upload-artifact@v1 37 | with: 38 | name: coverage 39 | path: coverage 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | coverage 4 | node_modules 5 | .idea 6 | .vscode 7 | .github 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | .idea 3 | *.iml 4 | out 5 | gen 6 | 7 | # Unrelevant files and folders 8 | benchmark 9 | coverage 10 | test 11 | .travis.yml 12 | .gitignore 13 | *.log 14 | .vscode 15 | .codeclimate.yml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 NodeRedis 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/NodeRedis/node-redis-parser/workflows/Tests/badge.svg)](https://github.com/NodeRedis/node-redis-parser/actions) 2 | [![Coverage Status](https://coveralls.io/repos/github/NodeRedis/node-redis-parser/badge.svg?branch=)](https://coveralls.io/github/NodeRedis/node-redis-parser?branch=master) 3 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 4 | 5 | # redis-parser 6 | 7 | A high performance javascript redis parser built for [node_redis](https://github.com/NodeRedis/node_redis) and [ioredis](https://github.com/luin/ioredis). Parses all [RESP](http://redis.io/topics/protocol) data. 8 | 9 | ## Install 10 | 11 | Install with [NPM](https://npmjs.org/): 12 | 13 | npm install redis-parser 14 | 15 | ## Usage 16 | 17 | ```js 18 | const Parser = require('redis-parser'); 19 | 20 | const myParser = new Parser(options); 21 | ``` 22 | 23 | ### Options 24 | 25 | * `returnReply`: *function*; mandatory 26 | * `returnError`: *function*; mandatory 27 | * `returnFatalError`: *function*; optional, defaults to the returnError function 28 | * `returnBuffers`: *boolean*; optional, defaults to false 29 | * `stringNumbers`: *boolean*; optional, defaults to false 30 | 31 | ### Functions 32 | 33 | * `reset()`: reset the parser to it's initial state 34 | * `setReturnBuffers(boolean)`: set the returnBuffers option on/off without resetting the parser 35 | * `setStringNumbers(boolean)`: set the stringNumbers option on/off without resetting the parser 36 | 37 | ### Error classes 38 | 39 | * `RedisError` sub class of Error 40 | * `ReplyError` sub class of RedisError 41 | * `ParserError` sub class of RedisError 42 | 43 | All Redis errors will be returned as `ReplyErrors` while a parser error is returned as `ParserError`. 44 | All error classes can be imported by the npm `redis-errors` package. 45 | 46 | ### Example 47 | 48 | ```js 49 | const Parser = require("redis-parser"); 50 | 51 | class Library { 52 | returnReply(reply) { /* ... */ } 53 | returnError(err) { /* ... */ } 54 | returnFatalError(err) { /* ... */ } 55 | 56 | streamHandler() { 57 | this.stream.on('data', (buffer) => { 58 | // Here the data (e.g. `Buffer.from('$5\r\nHello\r\n'`)) 59 | // is passed to the parser and the result is passed to 60 | // either function depending on the provided data. 61 | parser.execute(buffer); 62 | }); 63 | } 64 | } 65 | 66 | const lib = new Library(); 67 | 68 | const parser = new Parser({ 69 | returnReply(reply) { 70 | lib.returnReply(reply); 71 | }, 72 | returnError(err) { 73 | lib.returnError(err); 74 | }, 75 | returnFatalError(err) { 76 | lib.returnFatalError(err); 77 | } 78 | }); 79 | ``` 80 | 81 | You do not have to use the returnFatalError function. Fatal errors will be returned in the normal error function in that case. 82 | 83 | And if you want to return buffers instead of strings, you can do this by adding the `returnBuffers` option. 84 | 85 | If you handle with big numbers that are to large for JS (Number.MAX_SAFE_INTEGER === 2^53 - 16) please use the `stringNumbers` option. That way all numbers are going to be returned as String and you can handle them safely. 86 | 87 | ```js 88 | // Same functions as in the first example 89 | 90 | const parser = new Parser({ 91 | returnReply(reply) { 92 | lib.returnReply(reply); 93 | }, 94 | returnError(err) { 95 | lib.returnError(err); 96 | }, 97 | returnBuffers: true, // All strings are returned as Buffer e.g. 98 | stringNumbers: true // All numbers are returned as String 99 | }); 100 | 101 | // The streamHandler as above 102 | ``` 103 | 104 | ## Protocol errors 105 | 106 | To handle protocol errors (this is very unlikely to happen) gracefully you should add the returnFatalError option, reject any still running command (they might have been processed properly but the reply is just wrong), destroy the socket and reconnect. Note that while doing this no new command may be added, so all new commands have to be buffered in the meantime, otherwise a chunk might still contain partial data of a following command that was already processed properly but answered in the same chunk as the command that resulted in the protocol error. 107 | 108 | ## Contribute 109 | 110 | The parser is highly optimized but there may still be further optimizations possible. 111 | 112 | npm install 113 | npm test 114 | npm run benchmark 115 | 116 | Currently the benchmark compares the performance against the hiredis parser: 117 | 118 | HIREDIS: $ multiple chunks in a bulk string x 1,169,386 ops/sec ±1.24% (92 runs sampled) 119 | JS PARSER: $ multiple chunks in a bulk string x 1,354,290 ops/sec ±1.69% (88 runs sampled) 120 | HIREDIS BUF: $ multiple chunks in a bulk string x 633,639 ops/sec ±2.64% (84 runs sampled) 121 | JS PARSER BUF: $ multiple chunks in a bulk string x 1,783,922 ops/sec ±0.47% (94 runs sampled) 122 | 123 | HIREDIS: + multiple chunks in a string x 2,394,900 ops/sec ±0.31% (93 runs sampled) 124 | JS PARSER: + multiple chunks in a string x 2,264,354 ops/sec ±0.29% (94 runs sampled) 125 | HIREDIS BUF: + multiple chunks in a string x 953,733 ops/sec ±2.03% (82 runs sampled) 126 | JS PARSER BUF: + multiple chunks in a string x 2,298,458 ops/sec ±0.79% (96 runs sampled) 127 | 128 | HIREDIS: $ 4mb bulk string x 152 ops/sec ±2.03% (72 runs sampled) 129 | JS PARSER: $ 4mb bulk string x 971 ops/sec ±0.79% (86 runs sampled) 130 | HIREDIS BUF: $ 4mb bulk string x 169 ops/sec ±2.25% (71 runs sampled) 131 | JS PARSER BUF: $ 4mb bulk string x 797 ops/sec ±7.08% (77 runs sampled) 132 | 133 | HIREDIS: + simple string x 3,341,956 ops/sec ±1.01% (94 runs sampled) 134 | JS PARSER: + simple string x 5,979,545 ops/sec ±0.38% (96 runs sampled) 135 | HIREDIS BUF: + simple string x 1,031,745 ops/sec ±2.17% (76 runs sampled) 136 | JS PARSER BUF: + simple string x 6,960,184 ops/sec ±0.28% (93 runs sampled) 137 | 138 | HIREDIS: : integer x 3,897,626 ops/sec ±0.42% (91 runs sampled) 139 | JS PARSER: : integer x 37,035,812 ops/sec ±0.32% (94 runs sampled) 140 | JS PARSER STR: : integer x 25,515,070 ops/sec ±1.79% (83 runs sampled) 141 | 142 | HIREDIS: : big integer x 3,036,704 ops/sec ±0.47% (92 runs sampled) 143 | JS PARSER: : big integer x 10,616,464 ops/sec ±0.94% (94 runs sampled) 144 | JS PARSER STR: : big integer x 7,098,146 ops/sec ±0.47% (94 runs sampled) 145 | 146 | HIREDIS: * array x 51,542 ops/sec ±0.35% (94 runs sampled) 147 | JS PARSER: * array x 87,090 ops/sec ±2.17% (94 runs sampled) 148 | HIREDIS BUF: * array x 11,733 ops/sec ±1.80% (80 runs sampled) 149 | JS PARSER BUF: * array x 149,430 ops/sec ±1.50% (88 runs sampled) 150 | 151 | HIREDIS: * big nested array x 247 ops/sec ±0.93% (73 runs sampled) 152 | JS PARSER: * big nested array x 286 ops/sec ±0.79% (83 runs sampled) 153 | HIREDIS BUF: * big nested array x 217 ops/sec ±1.80% (73 runs sampled) 154 | JS PARSER BUF: * big nested array x 175 ops/sec ±2.49% (37 runs sampled) 155 | 156 | HIREDIS: - error x 108,110 ops/sec ±0.63% (84 runs sampled) 157 | JS PARSER: - error x 172,665 ops/sec ±0.57% (85 runs sampled) 158 | 159 | Platform info: 160 | OSX 10.12.6 161 | Node.js 10.0.0 162 | Intel(R) Core(TM) i7-5600U CPU 163 | 164 | ## License 165 | 166 | [MIT](./LICENSE) 167 | -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint handle-callback-err: 0 */ 4 | 5 | const Benchmark = require('benchmark') 6 | const suite = new Benchmark.Suite() 7 | const Parser = require('./../') 8 | const Buffer = require('buffer').Buffer 9 | 10 | function returnError (error) {} 11 | function checkReply (error, res) {} 12 | 13 | const startBuffer = Buffer.from('$100\r\nabcdefghij') 14 | const chunkBuffer = Buffer.from('abcdefghijabcdefghijabcdefghij') 15 | const stringBuffer = Buffer.from('+testing a simple string\r\n') 16 | const integerBuffer = Buffer.from(':1237884\r\n') 17 | const bigIntegerBuffer = Buffer.from(':184467440737095516171234567890\r\n') // 2^64 + 1 18 | const errorBuffer = Buffer.from('-Error ohnoesitbroke\r\n') 19 | const endBuffer = Buffer.from('\r\n') 20 | const lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ' + 21 | 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' + 22 | 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ' + 23 | 'ut aliquip ex ea commodo consequat. Duis aute irure dolor in' // 256 chars 24 | const bigStringArray = (new Array(Math.pow(2, 16) / lorem.length).join(lorem + ' ')).split(' ') // Math.pow(2, 16) chars long 25 | const startBigBuffer = Buffer.from('$' + (4 * 1024 * 1024) + '\r\n') 26 | 27 | const chunks = new Array(64) 28 | for (var i = 0; i < 64; i++) { 29 | chunks[i] = Buffer.from(bigStringArray.join(' ') + '.') // Math.pow(2, 16) chars long 30 | } 31 | 32 | const arraySize = 100 33 | var array = '*' + arraySize + '\r\n' 34 | var size = 0 35 | for (i = 0; i < arraySize; i++) { 36 | array += '$' 37 | size = (Math.random() * 10 | 0) + 1 38 | array += size + '\r\n' + lorem.slice(0, size) + '\r\n' 39 | } 40 | 41 | const arrayBuffer = Buffer.from(array) 42 | 43 | const bigArraySize = 160 44 | const bigArrayChunks = [Buffer.from('*1\r\n*1\r\n*' + bigArraySize)] 45 | for (i = 0; i < bigArraySize; i++) { 46 | // A chunk has a maximum size of 2^16 bytes. 47 | size = 65000 + i 48 | if (i % 2) { 49 | // The "x" in the beginning is important to prevent benchmark manipulation due to a minor JSParser optimization 50 | bigArrayChunks.push(Buffer.from('x\r\n$' + size + '\r\n' + Array(size + 1).join('a') + '\r\n:' + (Math.random() * 1000000 | 0))) 51 | } else { 52 | bigArrayChunks.push(Buffer.from('\r\n+this is some short text about nothing\r\n:' + size + '\r\n$' + size + '\r\n' + Array(size).join('b'))) 53 | } 54 | } 55 | bigArrayChunks.push(Buffer.from('\r\n')) 56 | 57 | const chunkedStringPart1 = Buffer.from('+foobar') 58 | const chunkedStringPart2 = Buffer.from('bazEND\r\n') 59 | 60 | const options = { 61 | returnReply: checkReply, 62 | returnError: returnError, 63 | returnFatalError: returnError 64 | } 65 | const parser = new Parser(options) 66 | 67 | options.returnBuffers = true 68 | const parserBuffer = new Parser(options) 69 | 70 | delete options.returnBuffers 71 | options.stringNumbers = true 72 | const parserStr = new Parser(options) 73 | 74 | // BULK STRINGS 75 | 76 | suite.add('JS PARSER: $ multiple chunks in a bulk string', function () { 77 | parser.execute(startBuffer) 78 | parser.execute(chunkBuffer) 79 | parser.execute(chunkBuffer) 80 | parser.execute(chunkBuffer) 81 | parser.execute(endBuffer) 82 | }) 83 | 84 | suite.add('JS PARSER BUF: $ multiple chunks in a bulk string', function () { 85 | parserBuffer.execute(startBuffer) 86 | parserBuffer.execute(chunkBuffer) 87 | parserBuffer.execute(chunkBuffer) 88 | parserBuffer.execute(chunkBuffer) 89 | parserBuffer.execute(endBuffer) 90 | }) 91 | 92 | // CHUNKED STRINGS 93 | 94 | suite.add('JS PARSER: + multiple chunks in a string', function () { 95 | parser.execute(chunkedStringPart1) 96 | parser.execute(chunkedStringPart2) 97 | }) 98 | 99 | suite.add('JS PARSER BUF: + multiple chunks in a string', function () { 100 | parserBuffer.execute(chunkedStringPart1) 101 | parserBuffer.execute(chunkedStringPart2) 102 | }) 103 | 104 | // BIG BULK STRING 105 | 106 | suite.add('JS PARSER: $ 4mb bulk string', function () { 107 | parser.execute(startBigBuffer) 108 | for (var i = 0; i < 64; i++) { 109 | parser.execute(chunks[i]) 110 | } 111 | parser.execute(endBuffer) 112 | }) 113 | 114 | suite.add('JS PARSER BUF: $ 4mb bulk string', function () { 115 | parserBuffer.execute(startBigBuffer) 116 | for (var i = 0; i < 64; i++) { 117 | parserBuffer.execute(chunks[i]) 118 | } 119 | parserBuffer.execute(endBuffer) 120 | }) 121 | 122 | // STRINGS 123 | 124 | suite.add('JS PARSER: + simple string', function () { 125 | parser.execute(stringBuffer) 126 | }) 127 | 128 | suite.add('JS PARSER BUF: + simple string', function () { 129 | parserBuffer.execute(stringBuffer) 130 | }) 131 | 132 | // INTEGERS 133 | 134 | suite.add('JS PARSER: : integer', function () { 135 | parser.execute(integerBuffer) 136 | }) 137 | 138 | suite.add('JS PARSER STR: : integer', function () { 139 | parserStr.execute(integerBuffer) 140 | }) 141 | 142 | // BIG INTEGER 143 | 144 | suite.add('JS PARSER: : big integer', function () { 145 | parser.execute(bigIntegerBuffer) 146 | }) 147 | 148 | suite.add('JS PARSER STR: : big integer', function () { 149 | parserStr.execute(bigIntegerBuffer) 150 | }) 151 | 152 | // ARRAYS 153 | 154 | suite.add('JS PARSER: * array', function () { 155 | parser.execute(arrayBuffer) 156 | }) 157 | 158 | suite.add('JS PARSER BUF: * array', function () { 159 | parserBuffer.execute(arrayBuffer) 160 | }) 161 | 162 | // BIG NESTED ARRAYS 163 | 164 | suite.add('JS PARSER: * big nested array', function () { 165 | for (var i = 0; i < bigArrayChunks.length; i++) { 166 | parser.execute(bigArrayChunks[i]) 167 | } 168 | }) 169 | 170 | suite.add('JS PARSER BUF: * big nested array', function () { 171 | for (var i = 0; i < bigArrayChunks.length; i++) { 172 | parserBuffer.execute(bigArrayChunks[i]) 173 | } 174 | }) 175 | 176 | // ERRORS 177 | 178 | suite.add('JS PARSER: - error', function () { 179 | parser.execute(errorBuffer) 180 | }) 181 | 182 | // add listeners 183 | suite.on('cycle', function (event) { 184 | console.log(String(event.target)) 185 | }) 186 | 187 | suite.on('complete', function () { 188 | console.log('\n\nFastest is ' + this.filter('fastest').map('name')) 189 | // Do not wait for the bufferPool to shrink 190 | process.exit() 191 | }) 192 | 193 | suite.run({ delay: 1, minSamples: 150 }) 194 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v.3.0.0 - 25 May, 2017 4 | 5 | Breaking Changes 6 | 7 | - Drop support for Node.js < 4 8 | - Removed support for hiredis completely 9 | 10 | Internals 11 | 12 | - Due to the changes to ES6 the error performance improved by factor 2-3x 13 | - Improved length calculation performance (bulk strings + arrays) 14 | 15 | Features 16 | 17 | - The parser now handles weird input graceful 18 | 19 | ## v.2.6.0 - 03 Apr, 2017 20 | 21 | Internals 22 | 23 | - Use Buffer.allocUnsafe instead of new Buffer() with modern Node.js versions 24 | 25 | ## v.2.5.0 - 11 Mar, 2017 26 | 27 | Features 28 | 29 | - Added a `ParserError` class to differentiate them to ReplyErrors. The class is also exported 30 | 31 | Bugfixes 32 | 33 | - All errors now show their error message again next to the error name in the stack trace 34 | - ParserErrors now show the offset and buffer attributes while being logged 35 | 36 | ## v.2.4.1 - 05 Feb, 2017 37 | 38 | Bugfixes 39 | 40 | - Fixed minimal memory consumption overhead for chunked buffers 41 | 42 | ## v.2.4.0 - 25 Jan, 2017 43 | 44 | Features 45 | 46 | - Added `reset` function to reset the parser to it's initial values 47 | - Added `setReturnBuffers` function to reset the returnBuffers option (Only for the JSParser) 48 | - Added `setStringNumbers` function to reset the stringNumbers option (Only for the JSParser) 49 | - All Errors are now of sub classes of the new `RedisError` class. It is also exported. 50 | - Improved bulk string chunked data handling performance 51 | 52 | Bugfixes 53 | 54 | - Parsing time for big nested arrays is now linear 55 | 56 | ## v.2.3.0 - 25 Nov, 2016 57 | 58 | Features 59 | 60 | - Parsing time for big arrays (e.g. 4mb+) is now linear and works well for arbitrary array sizes 61 | 62 | This case is a magnitude faster than before 63 | 64 | OLD STR: * big array x 1.09 ops/sec ±2.15% (7 runs sampled) 65 | OLD BUF: * big array x 1.23 ops/sec ±2.67% (8 runs sampled) 66 | 67 | NEW STR: * big array x 273 ops/sec ±2.09% (85 runs sampled) 68 | NEW BUF: * big array x 259 ops/sec ±1.32% (85 runs sampled) 69 | (~10mb array with 1000 entries) 70 | 71 | ## v.2.2.0 - 18 Nov, 2016 72 | 73 | Features 74 | 75 | - Improve `stringNumbers` parsing performance by up to 100% 76 | 77 | Bugfixes 78 | 79 | - Do not unref the interval anymore due to issues with NodeJS 80 | 81 | ## v.2.1.1 - 31 Oct, 2016 82 | 83 | Bugfixes 84 | 85 | - Remove erroneously added const to support Node.js 0.10 86 | 87 | ## v.2.1.0 - 30 Oct, 2016 88 | 89 | Features 90 | 91 | - Improve parser errors by adding more detailed information to them 92 | - Accept manipulated Object.prototypes 93 | - Unref the interval if used 94 | 95 | ## v.2.0.4 - 21 Jul, 2016 96 | 97 | Bugfixes 98 | 99 | - Fixed multi byte characters getting corrupted 100 | 101 | ## v.2.0.3 - 17 Jun, 2016 102 | 103 | Bugfixes 104 | 105 | - Fixed parser not working with huge buffers (e.g. 300 MB) 106 | 107 | ## v.2.0.2 - 08 Jun, 2016 108 | 109 | Bugfixes 110 | 111 | - Fixed parser with returnBuffers option returning corrupted data 112 | 113 | ## v.2.0.1 - 04 Jun, 2016 114 | 115 | Bugfixes 116 | 117 | - Fixed multiple parsers working concurrently resulting in faulty data in some cases 118 | 119 | ## v.2.0.0 - 29 May, 2016 120 | 121 | The javascript parser got completely rewritten by [Michael Diarmid](https://github.com/Salakar) and [Ruben Bridgewater](https://github.com/BridgeAR) and is now a lot faster than the hiredis parser. 122 | Therefore the hiredis parser was deprecated and should only be used for testing purposes and benchmarking comparison. 123 | 124 | All Errors returned by the parser are from now on of class ReplyError 125 | 126 | Features 127 | 128 | - Improved performance by up to 15x as fast as before 129 | - Improved options validation 130 | - Added ReplyError Class 131 | - Added parser benchmark 132 | - Switched default parser from hiredis to JS, no matter if hiredis is installed or not 133 | 134 | Removed 135 | 136 | - Deprecated hiredis support 137 | 138 | ## v.1.3.0 - 27 Mar, 2016 139 | 140 | Features 141 | 142 | - Added `auto` as parser name option to check what parser is available 143 | - Non existing requested parsers falls back into auto mode instead of always choosing the JS parser 144 | 145 | ## v.1.2.0 - 27 Mar, 2016 146 | 147 | Features 148 | 149 | - Added `stringNumbers` option to make sure all numbers are returned as string instead of a js number for precision 150 | - The parser is from now on going to print warnings if a parser is explicitly requested that does not exist and gracefully chooses the JS parser 151 | 152 | ## v.1.1.0 - 26 Jan, 2016 153 | 154 | Features 155 | 156 | - The parser is from now on going to reset itself on protocol errors 157 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('./lib/parser') 4 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Buffer = require('buffer').Buffer 4 | const StringDecoder = require('string_decoder').StringDecoder 5 | const decoder = new StringDecoder() 6 | const errors = require('redis-errors') 7 | const ReplyError = errors.ReplyError 8 | const ParserError = errors.ParserError 9 | var bufferPool = Buffer.allocUnsafe(32 * 1024) 10 | var bufferOffset = 0 11 | var interval = null 12 | var counter = 0 13 | var notDecreased = 0 14 | 15 | /** 16 | * Used for integer numbers only 17 | * @param {JavascriptRedisParser} parser 18 | * @returns {undefined|number} 19 | */ 20 | function parseSimpleNumbers (parser) { 21 | const length = parser.buffer.length - 1 22 | var offset = parser.offset 23 | var number = 0 24 | var sign = 1 25 | 26 | if (parser.buffer[offset] === 45) { 27 | sign = -1 28 | offset++ 29 | } 30 | 31 | while (offset < length) { 32 | const c1 = parser.buffer[offset++] 33 | if (c1 === 13) { // \r\n 34 | parser.offset = offset + 1 35 | return sign * number 36 | } 37 | number = (number * 10) + (c1 - 48) 38 | } 39 | } 40 | 41 | /** 42 | * Used for integer numbers in case of the returnNumbers option 43 | * 44 | * Reading the string as parts of n SMI is more efficient than 45 | * using a string directly. 46 | * 47 | * @param {JavascriptRedisParser} parser 48 | * @returns {undefined|string} 49 | */ 50 | function parseStringNumbers (parser) { 51 | const length = parser.buffer.length - 1 52 | var offset = parser.offset 53 | var number = 0 54 | var res = '' 55 | 56 | if (parser.buffer[offset] === 45) { 57 | res += '-' 58 | offset++ 59 | } 60 | 61 | while (offset < length) { 62 | var c1 = parser.buffer[offset++] 63 | if (c1 === 13) { // \r\n 64 | parser.offset = offset + 1 65 | if (number !== 0) { 66 | res += number 67 | } 68 | return res 69 | } else if (number > 429496728) { 70 | res += (number * 10) + (c1 - 48) 71 | number = 0 72 | } else if (c1 === 48 && number === 0) { 73 | res += 0 74 | } else { 75 | number = (number * 10) + (c1 - 48) 76 | } 77 | } 78 | } 79 | 80 | /** 81 | * Parse a '+' redis simple string response but forward the offsets 82 | * onto convertBufferRange to generate a string. 83 | * @param {JavascriptRedisParser} parser 84 | * @returns {undefined|string|Buffer} 85 | */ 86 | function parseSimpleString (parser) { 87 | const start = parser.offset 88 | const buffer = parser.buffer 89 | const length = buffer.length - 1 90 | var offset = start 91 | 92 | while (offset < length) { 93 | if (buffer[offset++] === 13) { // \r\n 94 | parser.offset = offset + 1 95 | if (parser.optionReturnBuffers === true) { 96 | return parser.buffer.slice(start, offset - 1) 97 | } 98 | return parser.buffer.toString('utf8', start, offset - 1) 99 | } 100 | } 101 | } 102 | 103 | /** 104 | * Returns the read length 105 | * @param {JavascriptRedisParser} parser 106 | * @returns {undefined|number} 107 | */ 108 | function parseLength (parser) { 109 | const length = parser.buffer.length - 1 110 | var offset = parser.offset 111 | var number = 0 112 | 113 | while (offset < length) { 114 | const c1 = parser.buffer[offset++] 115 | if (c1 === 13) { 116 | parser.offset = offset + 1 117 | return number 118 | } 119 | number = (number * 10) + (c1 - 48) 120 | } 121 | } 122 | 123 | /** 124 | * Parse a ':' redis integer response 125 | * 126 | * If stringNumbers is activated the parser always returns numbers as string 127 | * This is important for big numbers (number > Math.pow(2, 53)) as js numbers 128 | * are 64bit floating point numbers with reduced precision 129 | * 130 | * @param {JavascriptRedisParser} parser 131 | * @returns {undefined|number|string} 132 | */ 133 | function parseInteger (parser) { 134 | if (parser.optionStringNumbers === true) { 135 | return parseStringNumbers(parser) 136 | } 137 | return parseSimpleNumbers(parser) 138 | } 139 | 140 | /** 141 | * Parse a '$' redis bulk string response 142 | * @param {JavascriptRedisParser} parser 143 | * @returns {undefined|null|string} 144 | */ 145 | function parseBulkString (parser) { 146 | const length = parseLength(parser) 147 | if (length === undefined) { 148 | return 149 | } 150 | if (length < 0) { 151 | return null 152 | } 153 | const offset = parser.offset + length 154 | if (offset + 2 > parser.buffer.length) { 155 | parser.bigStrSize = offset + 2 156 | parser.totalChunkSize = parser.buffer.length 157 | parser.bufferCache.push(parser.buffer) 158 | return 159 | } 160 | const start = parser.offset 161 | parser.offset = offset + 2 162 | if (parser.optionReturnBuffers === true) { 163 | return parser.buffer.slice(start, offset) 164 | } 165 | return parser.buffer.toString('utf8', start, offset) 166 | } 167 | 168 | /** 169 | * Parse a '-' redis error response 170 | * @param {JavascriptRedisParser} parser 171 | * @returns {ReplyError} 172 | */ 173 | function parseError (parser) { 174 | var string = parseSimpleString(parser) 175 | if (string !== undefined) { 176 | if (parser.optionReturnBuffers === true) { 177 | string = string.toString() 178 | } 179 | return new ReplyError(string) 180 | } 181 | } 182 | 183 | /** 184 | * Parsing error handler, resets parser buffer 185 | * @param {JavascriptRedisParser} parser 186 | * @param {number} type 187 | * @returns {undefined} 188 | */ 189 | function handleError (parser, type) { 190 | const err = new ParserError( 191 | 'Protocol error, got ' + JSON.stringify(String.fromCharCode(type)) + ' as reply type byte', 192 | JSON.stringify(parser.buffer), 193 | parser.offset 194 | ) 195 | parser.buffer = null 196 | parser.returnFatalError(err) 197 | } 198 | 199 | /** 200 | * Parse a '*' redis array response 201 | * @param {JavascriptRedisParser} parser 202 | * @returns {undefined|null|any[]} 203 | */ 204 | function parseArray (parser) { 205 | const length = parseLength(parser) 206 | if (length === undefined) { 207 | return 208 | } 209 | if (length < 0) { 210 | return null 211 | } 212 | const responses = new Array(length) 213 | return parseArrayElements(parser, responses, 0) 214 | } 215 | 216 | /** 217 | * Push a partly parsed array to the stack 218 | * 219 | * @param {JavascriptRedisParser} parser 220 | * @param {any[]} array 221 | * @param {number} pos 222 | * @returns {undefined} 223 | */ 224 | function pushArrayCache (parser, array, pos) { 225 | parser.arrayCache.push(array) 226 | parser.arrayPos.push(pos) 227 | } 228 | 229 | /** 230 | * Parse chunked redis array response 231 | * @param {JavascriptRedisParser} parser 232 | * @returns {undefined|any[]} 233 | */ 234 | function parseArrayChunks (parser) { 235 | var arr = parser.arrayCache.pop() 236 | var pos = parser.arrayPos.pop() 237 | if (parser.arrayCache.length) { 238 | const res = parseArrayChunks(parser) 239 | if (res === undefined) { 240 | pushArrayCache(parser, arr, pos) 241 | return 242 | } 243 | arr[pos++] = res 244 | } 245 | return parseArrayElements(parser, arr, pos) 246 | } 247 | 248 | /** 249 | * Parse redis array response elements 250 | * @param {JavascriptRedisParser} parser 251 | * @param {Array} responses 252 | * @param {number} i 253 | * @returns {undefined|null|any[]} 254 | */ 255 | function parseArrayElements (parser, responses, i) { 256 | const bufferLength = parser.buffer.length 257 | while (i < responses.length) { 258 | const offset = parser.offset 259 | if (parser.offset >= bufferLength) { 260 | pushArrayCache(parser, responses, i) 261 | return 262 | } 263 | const response = parseType(parser, parser.buffer[parser.offset++]) 264 | if (response === undefined) { 265 | if (!(parser.arrayCache.length || parser.bufferCache.length)) { 266 | parser.offset = offset 267 | } 268 | pushArrayCache(parser, responses, i) 269 | return 270 | } 271 | responses[i] = response 272 | i++ 273 | } 274 | 275 | return responses 276 | } 277 | 278 | /** 279 | * Called the appropriate parser for the specified type. 280 | * 281 | * 36: $ 282 | * 43: + 283 | * 42: * 284 | * 58: : 285 | * 45: - 286 | * 287 | * @param {JavascriptRedisParser} parser 288 | * @param {number} type 289 | * @returns {*} 290 | */ 291 | function parseType (parser, type) { 292 | switch (type) { 293 | case 36: 294 | return parseBulkString(parser) 295 | case 43: 296 | return parseSimpleString(parser) 297 | case 42: 298 | return parseArray(parser) 299 | case 58: 300 | return parseInteger(parser) 301 | case 45: 302 | return parseError(parser) 303 | default: 304 | return handleError(parser, type) 305 | } 306 | } 307 | 308 | /** 309 | * Decrease the bufferPool size over time 310 | * 311 | * Balance between increasing and decreasing the bufferPool. 312 | * Decrease the bufferPool by 10% by removing the first 10% of the current pool. 313 | * @returns {undefined} 314 | */ 315 | function decreaseBufferPool () { 316 | if (bufferPool.length > 50 * 1024) { 317 | if (counter === 1 || notDecreased > counter * 2) { 318 | const minSliceLen = Math.floor(bufferPool.length / 10) 319 | const sliceLength = minSliceLen < bufferOffset 320 | ? bufferOffset 321 | : minSliceLen 322 | bufferOffset = 0 323 | bufferPool = bufferPool.slice(sliceLength, bufferPool.length) 324 | } else { 325 | notDecreased++ 326 | counter-- 327 | } 328 | } else { 329 | clearInterval(interval) 330 | counter = 0 331 | notDecreased = 0 332 | interval = null 333 | } 334 | } 335 | 336 | /** 337 | * Check if the requested size fits in the current bufferPool. 338 | * If it does not, reset and increase the bufferPool accordingly. 339 | * 340 | * @param {number} length 341 | * @returns {undefined} 342 | */ 343 | function resizeBuffer (length) { 344 | if (bufferPool.length < length + bufferOffset) { 345 | const multiplier = length > 1024 * 1024 * 75 ? 2 : 3 346 | if (bufferOffset > 1024 * 1024 * 111) { 347 | bufferOffset = 1024 * 1024 * 50 348 | } 349 | bufferPool = Buffer.allocUnsafe(length * multiplier + bufferOffset) 350 | bufferOffset = 0 351 | counter++ 352 | if (interval === null) { 353 | interval = setInterval(decreaseBufferPool, 50) 354 | } 355 | } 356 | } 357 | 358 | /** 359 | * Concat a bulk string containing multiple chunks 360 | * 361 | * Notes: 362 | * 1) The first chunk might contain the whole bulk string including the \r 363 | * 2) We are only safe to fully add up elements that are neither the first nor any of the last two elements 364 | * 365 | * @param {JavascriptRedisParser} parser 366 | * @returns {String} 367 | */ 368 | function concatBulkString (parser) { 369 | const list = parser.bufferCache 370 | const oldOffset = parser.offset 371 | var chunks = list.length 372 | var offset = parser.bigStrSize - parser.totalChunkSize 373 | parser.offset = offset 374 | if (offset <= 2) { 375 | if (chunks === 2) { 376 | return list[0].toString('utf8', oldOffset, list[0].length + offset - 2) 377 | } 378 | chunks-- 379 | offset = list[list.length - 2].length + offset 380 | } 381 | var res = decoder.write(list[0].slice(oldOffset)) 382 | for (var i = 1; i < chunks - 1; i++) { 383 | res += decoder.write(list[i]) 384 | } 385 | res += decoder.end(list[i].slice(0, offset - 2)) 386 | return res 387 | } 388 | 389 | /** 390 | * Concat the collected chunks from parser.bufferCache. 391 | * 392 | * Increases the bufferPool size beforehand if necessary. 393 | * 394 | * @param {JavascriptRedisParser} parser 395 | * @returns {Buffer} 396 | */ 397 | function concatBulkBuffer (parser) { 398 | const list = parser.bufferCache 399 | const oldOffset = parser.offset 400 | const length = parser.bigStrSize - oldOffset - 2 401 | var chunks = list.length 402 | var offset = parser.bigStrSize - parser.totalChunkSize 403 | parser.offset = offset 404 | if (offset <= 2) { 405 | if (chunks === 2) { 406 | return list[0].slice(oldOffset, list[0].length + offset - 2) 407 | } 408 | chunks-- 409 | offset = list[list.length - 2].length + offset 410 | } 411 | resizeBuffer(length) 412 | const start = bufferOffset 413 | list[0].copy(bufferPool, start, oldOffset, list[0].length) 414 | bufferOffset += list[0].length - oldOffset 415 | for (var i = 1; i < chunks - 1; i++) { 416 | list[i].copy(bufferPool, bufferOffset) 417 | bufferOffset += list[i].length 418 | } 419 | list[i].copy(bufferPool, bufferOffset, 0, offset - 2) 420 | bufferOffset += offset - 2 421 | return bufferPool.slice(start, bufferOffset) 422 | } 423 | 424 | class JavascriptRedisParser { 425 | /** 426 | * Javascript Redis Parser constructor 427 | * @param {{returnError: Function, returnReply: Function, returnFatalError?: Function, returnBuffers: boolean, stringNumbers: boolean }} options 428 | * @constructor 429 | */ 430 | constructor (options) { 431 | if (!options) { 432 | throw new TypeError('Options are mandatory.') 433 | } 434 | if (typeof options.returnError !== 'function' || typeof options.returnReply !== 'function') { 435 | throw new TypeError('The returnReply and returnError options have to be functions.') 436 | } 437 | this.setReturnBuffers(!!options.returnBuffers) 438 | this.setStringNumbers(!!options.stringNumbers) 439 | this.returnError = options.returnError 440 | this.returnFatalError = options.returnFatalError || options.returnError 441 | this.returnReply = options.returnReply 442 | this.reset() 443 | } 444 | 445 | /** 446 | * Reset the parser values to the initial state 447 | * 448 | * @returns {undefined} 449 | */ 450 | reset () { 451 | this.offset = 0 452 | this.buffer = null 453 | this.bigStrSize = 0 454 | this.totalChunkSize = 0 455 | this.bufferCache = [] 456 | this.arrayCache = [] 457 | this.arrayPos = [] 458 | } 459 | 460 | /** 461 | * Set the returnBuffers option 462 | * 463 | * @param {boolean} returnBuffers 464 | * @returns {undefined} 465 | */ 466 | setReturnBuffers (returnBuffers) { 467 | if (typeof returnBuffers !== 'boolean') { 468 | throw new TypeError('The returnBuffers argument has to be a boolean') 469 | } 470 | this.optionReturnBuffers = returnBuffers 471 | } 472 | 473 | /** 474 | * Set the stringNumbers option 475 | * 476 | * @param {boolean} stringNumbers 477 | * @returns {undefined} 478 | */ 479 | setStringNumbers (stringNumbers) { 480 | if (typeof stringNumbers !== 'boolean') { 481 | throw new TypeError('The stringNumbers argument has to be a boolean') 482 | } 483 | this.optionStringNumbers = stringNumbers 484 | } 485 | 486 | /** 487 | * Parse the redis buffer 488 | * @param {Buffer} buffer 489 | * @returns {undefined} 490 | */ 491 | execute (buffer) { 492 | if (this.buffer === null) { 493 | this.buffer = buffer 494 | this.offset = 0 495 | } else if (this.bigStrSize === 0) { 496 | const oldLength = this.buffer.length 497 | const remainingLength = oldLength - this.offset 498 | const newBuffer = Buffer.allocUnsafe(remainingLength + buffer.length) 499 | this.buffer.copy(newBuffer, 0, this.offset, oldLength) 500 | buffer.copy(newBuffer, remainingLength, 0, buffer.length) 501 | this.buffer = newBuffer 502 | this.offset = 0 503 | if (this.arrayCache.length) { 504 | const arr = parseArrayChunks(this) 505 | if (arr === undefined) { 506 | return 507 | } 508 | this.returnReply(arr) 509 | } 510 | } else if (this.totalChunkSize + buffer.length >= this.bigStrSize) { 511 | this.bufferCache.push(buffer) 512 | var tmp = this.optionReturnBuffers ? concatBulkBuffer(this) : concatBulkString(this) 513 | this.bigStrSize = 0 514 | this.bufferCache = [] 515 | this.buffer = buffer 516 | if (this.arrayCache.length) { 517 | this.arrayCache[0][this.arrayPos[0]++] = tmp 518 | tmp = parseArrayChunks(this) 519 | if (tmp === undefined) { 520 | return 521 | } 522 | } 523 | this.returnReply(tmp) 524 | } else { 525 | this.bufferCache.push(buffer) 526 | this.totalChunkSize += buffer.length 527 | return 528 | } 529 | 530 | while (this.offset < this.buffer.length) { 531 | const offset = this.offset 532 | const type = this.buffer[this.offset++] 533 | const response = parseType(this, type) 534 | if (response === undefined) { 535 | if (!(this.arrayCache.length || this.bufferCache.length)) { 536 | this.offset = offset 537 | } 538 | return 539 | } 540 | 541 | if (type === 45) { 542 | this.returnError(response) 543 | } else { 544 | this.returnReply(response) 545 | } 546 | } 547 | 548 | this.buffer = null 549 | } 550 | } 551 | 552 | module.exports = JavascriptRedisParser 553 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-parser", 3 | "version": "3.0.0", 4 | "description": "Javascript Redis protocol (RESP) parser", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "nyc --cache --preserve-comments mocha -- -R spec && nyc report --reporter=html", 8 | "benchmark": "node ./benchmark", 9 | "lint": "standard --fix", 10 | "posttest": "npm run lint && npm run coverage:check", 11 | "coveralls": "nyc report --reporter=text-lcov | coveralls", 12 | "coverage:check": "nyc check-coverage --branch 100 --statement 100" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/NodeRedis/node-redis-parser.git" 17 | }, 18 | "keywords": [ 19 | "redis", 20 | "protocol", 21 | "parser", 22 | "database", 23 | "javascript", 24 | "node", 25 | "nodejs", 26 | "resp" 27 | ], 28 | "files": [ 29 | "index.js", 30 | "lib" 31 | ], 32 | "engines": { 33 | "node": ">=4" 34 | }, 35 | "dependencies": { 36 | "redis-errors": "^1.2.0" 37 | }, 38 | "devDependencies": { 39 | "benchmark": "^2.1.0", 40 | "coveralls": "^3.0.10", 41 | "nyc": "^14.1.1", 42 | "mocha": "^6.1.1", 43 | "standard": "^11.0.1" 44 | }, 45 | "author": "Ruben Bridgewater", 46 | "license": "MIT", 47 | "bugs": { 48 | "url": "https://github.com/NodeRedis/node-redis-parser/issues" 49 | }, 50 | "homepage": "https://github.com/NodeRedis/node-redis-parser#readme", 51 | "directories": { 52 | "test": "test", 53 | "lib": "lib" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/parsers.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | /* eslint-disable no-new */ 5 | 6 | const assert = require('assert') 7 | const util = require('util') 8 | const Buffer = require('buffer').Buffer 9 | const JavascriptParser = require('../') 10 | const errors = require('redis-errors') 11 | const ReplyError = errors.ReplyError 12 | const ParserError = errors.ParserError 13 | const RedisError = errors.RedisError 14 | const parsers = [JavascriptParser] 15 | 16 | // Mock the not needed return functions 17 | function returnReply () { throw new Error('failed') } 18 | function returnError () { throw new Error('failed') } 19 | function returnFatalError (err) { throw err } 20 | 21 | describe('parsers', function () { 22 | describe('general parser functionality', function () { 23 | it('fail for missing options argument', function () { 24 | assert.throws(function () { 25 | new JavascriptParser() 26 | }, function (err) { 27 | assert(err instanceof TypeError) 28 | return true 29 | }) 30 | }) 31 | 32 | it('fail for faulty options properties', function () { 33 | assert.throws(function () { 34 | new JavascriptParser({ 35 | returnReply: returnReply, 36 | returnError: true 37 | }) 38 | }, function (err) { 39 | assert.strictEqual(err.message, 'The returnReply and returnError options have to be functions.') 40 | assert(err instanceof TypeError) 41 | return true 42 | }) 43 | }) 44 | 45 | it('should not fail for unknown options properties', function () { 46 | new JavascriptParser({ 47 | returnReply: returnReply, 48 | returnError: returnError, 49 | bla: 6 50 | }) 51 | }) 52 | 53 | it('reset returnBuffers option', function () { 54 | const res = 'test' 55 | let replyCount = 0 56 | function checkReply (reply) { 57 | if (replyCount === 0) { 58 | assert.strictEqual(reply, res) 59 | } else { 60 | assert.strictEqual(reply.inspect(), Buffer.from(res).inspect()) 61 | } 62 | replyCount++ 63 | } 64 | const parser = new JavascriptParser({ 65 | returnReply: checkReply, 66 | returnError: returnError 67 | }) 68 | parser.execute(Buffer.from('+test\r\n')) 69 | parser.execute(Buffer.from('+test')) 70 | parser.setReturnBuffers(true) 71 | assert.strictEqual(replyCount, 1) 72 | parser.execute(Buffer.from('\r\n$4\r\ntest\r\n')) 73 | assert.strictEqual(replyCount, 3) 74 | }) 75 | 76 | it('reset returnBuffers option with wrong input', function () { 77 | const parser = new JavascriptParser({ 78 | returnReply: returnReply, 79 | returnError: returnError 80 | }) 81 | assert.throws(function () { 82 | parser.setReturnBuffers(null) 83 | }, function (err) { 84 | assert.strictEqual(err.message, 'The returnBuffers argument has to be a boolean') 85 | assert(err instanceof TypeError) 86 | return true 87 | }) 88 | }) 89 | 90 | it('reset stringNumbers option', function () { 91 | const res = 123 92 | let replyCount = 0 93 | function checkReply (reply) { 94 | if (replyCount === 0) { 95 | assert.strictEqual(reply, res) 96 | } else { 97 | assert.strictEqual(reply, String(res)) 98 | } 99 | replyCount++ 100 | } 101 | const parser = new JavascriptParser({ 102 | returnReply: checkReply, 103 | returnError: returnError 104 | }) 105 | parser.execute(Buffer.from(':123\r\n')) 106 | assert.strictEqual(replyCount, 1) 107 | parser.setStringNumbers(true) 108 | parser.execute(Buffer.from(':123\r\n')) 109 | assert.strictEqual(replyCount, 2) 110 | }) 111 | 112 | it('reset stringNumbers option with wrong input', function () { 113 | const parser = new JavascriptParser({ 114 | returnReply: returnReply, 115 | returnError: returnError 116 | }) 117 | assert.throws(function () { 118 | parser.setStringNumbers(null) 119 | }, function (err) { 120 | assert.strictEqual(err.message, 'The stringNumbers argument has to be a boolean') 121 | assert(err instanceof TypeError) 122 | return true 123 | }) 124 | }) 125 | }) 126 | 127 | parsers.forEach(function (Parser) { 128 | function createBufferOfSize (parser, size, str) { 129 | if (size % 65536 !== 0) { 130 | throw new Error('Size may only be multiple of 65536') 131 | } 132 | str = str || '' 133 | const lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ' + 134 | 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' + 135 | 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ' + 136 | 'ut aliquip ex ea commodo consequat. Duis aute irure dolor in' // 256 chars 137 | const bigStringArray = (new Array(Math.pow(2, 16) / lorem.length).join(lorem + ' ')).split(' ') // Math.pow(2, 16) chars long 138 | const startBigBuffer = Buffer.from(str + '$' + size + '\r\n') 139 | const parts = size / 65536 140 | const chunks = new Array(parts) 141 | parser.execute(startBigBuffer) 142 | for (let i = 0; i < parts; i++) { 143 | chunks[i] = Buffer.from(bigStringArray.join(' ') + '.') // Math.pow(2, 16) chars long 144 | if (Parser.name === 'JavascriptRedisParser') { 145 | assert.strictEqual(parser.bufferCache.length, i + 1) 146 | } 147 | parser.execute(chunks[i]) 148 | } 149 | return chunks 150 | } 151 | 152 | function newParser (options, buffer) { 153 | if (typeof options === 'function') { 154 | options = { 155 | returnReply: options, 156 | returnBuffers: buffer === 'buffer' 157 | } 158 | } 159 | options.returnReply = options.returnReply || returnReply 160 | options.returnError = options.returnError || returnError 161 | options.returnFatalError = options.returnFatalError || returnFatalError 162 | return new Parser(options) 163 | } 164 | 165 | describe(Parser.name, function () { 166 | let replyCount = 0 167 | beforeEach(function () { 168 | replyCount = 0 169 | }) 170 | 171 | it('reset parser', function () { 172 | function checkReply (reply) { 173 | assert.strictEqual(reply, 'test') 174 | replyCount++ 175 | } 176 | const parser = newParser(checkReply) 177 | parser.execute(Buffer.from('$123\r\naaa')) 178 | parser.reset() 179 | parser.execute(Buffer.from('+test\r\n')) 180 | assert.strictEqual(replyCount, 1) 181 | }) 182 | 183 | it('weird things', function () { 184 | var replyCount = 0 185 | var results = [[], '', [0, null, '', 0, '', []], 9223372036854776, '☃', [1, 'OK', null], null, 12345, [], null, 't'] 186 | function checkReply (reply) { 187 | assert.deepEqual(results[replyCount], reply) 188 | replyCount++ 189 | } 190 | var parser = newParser(checkReply) 191 | parser.execute(Buffer.from('*0\r\n$0\r\n\r\n*6\r\n:\r\n$-1\r\n$0\r\n\r\n:-\r\n$')) 192 | assert.strictEqual(replyCount, 2) 193 | parser.execute(Buffer.from('\r\n\r\n*\r\n:9223372036854775\r\n$' + Buffer.byteLength('☃') + '\r\n☃\r\n')) 194 | assert.strictEqual(replyCount, 5) 195 | parser.execute(Buffer.from('*3\r\n:1\r\n+OK\r\n$-1\r\n')) 196 | assert.strictEqual(replyCount, 6) 197 | parser.execute(Buffer.from('$-5')) 198 | assert.strictEqual(replyCount, 6) 199 | parser.execute(Buffer.from('\r\n:12345\r\n*0\r\n*-1\r\n+t\r\n')) 200 | assert.strictEqual(replyCount, 11) 201 | }) 202 | 203 | it('should not set the bufferOffset to a negative value', function (done) { 204 | if (Parser.name === 'HiredisReplyParser') { 205 | return this.skip() 206 | } 207 | const size = 64 * 1024 208 | function checkReply (reply) {} 209 | const parser = newParser(checkReply, 'buffer') 210 | createBufferOfSize(parser, size * 11) 211 | createBufferOfSize(parser, size, '\r\n') 212 | parser.execute(Buffer.from('\r\n')) 213 | setTimeout(done, 425) 214 | }) 215 | 216 | it('multiple parsers do not interfere', function () { 217 | const results = [1234567890, 'foo bar baz', 'hello world'] 218 | function checkReply (reply) { 219 | assert.strictEqual(reply, results[replyCount]) 220 | replyCount++ 221 | } 222 | const parserOne = newParser(checkReply) 223 | const parserTwo = newParser(checkReply) 224 | parserOne.execute(Buffer.from('+foo ')) 225 | parserOne.execute(Buffer.from('bar ')) 226 | assert.strictEqual(replyCount, 0) 227 | parserTwo.execute(Buffer.from(':1234567890\r\n+hello ')) 228 | assert.strictEqual(replyCount, 1) 229 | parserTwo.execute(Buffer.from('wor')) 230 | parserOne.execute(Buffer.from('baz\r\n')) 231 | assert.strictEqual(replyCount, 2) 232 | parserTwo.execute(Buffer.from('ld\r\n')) 233 | assert.strictEqual(replyCount, 3) 234 | }) 235 | 236 | it('multiple parsers do not interfere with bulk strings in arrays', function () { 237 | const results = [['foo', 'foo bar baz'], [1234567890, 'hello world', 'the end'], 'ttttttttttttttttttttttttttttttttttttttttttttttt'] 238 | function checkReply (reply) { 239 | assert.deepEqual(reply, results[replyCount]) 240 | replyCount++ 241 | } 242 | const parserOne = newParser(checkReply) 243 | const parserTwo = newParser(checkReply) 244 | parserOne.execute(Buffer.from('*2\r\n+foo\r\n$11\r\nfoo ')) 245 | parserOne.execute(Buffer.from('bar ')) 246 | assert.strictEqual(replyCount, 0) 247 | parserTwo.execute(Buffer.from('*3\r\n:1234567890\r\n$11\r\nhello ')) 248 | assert.strictEqual(replyCount, 0) 249 | parserOne.execute(Buffer.from('baz\r\n+ttttttttttttttttttttttttt')) 250 | assert.strictEqual(replyCount, 1) 251 | parserTwo.execute(Buffer.from('wor')) 252 | parserTwo.execute(Buffer.from('ld\r\n')) 253 | assert.strictEqual(replyCount, 1) 254 | parserTwo.execute(Buffer.from('+the end\r\n')) 255 | assert.strictEqual(replyCount, 2) 256 | parserOne.execute(Buffer.from('tttttttttttttttttttttt\r\n')) 257 | }) 258 | 259 | it('returned buffers do not get mutated', function () { 260 | const results = [Buffer.from('aaaaaaaaaa'), Buffer.from('zzzzzzzzzz')] 261 | function checkReply (reply) { 262 | assert.deepEqual(results[replyCount], reply) 263 | results[replyCount] = reply 264 | replyCount++ 265 | } 266 | const parser = newParser(checkReply, 'buffer') 267 | parser.execute(Buffer.from('$10\r\naaaaa')) 268 | parser.execute(Buffer.from('aaaaa\r\n')) 269 | assert.strictEqual(replyCount, 1) 270 | parser.execute(Buffer.from('$10\r\nzzzzz')) 271 | parser.execute(Buffer.from('zzzzz\r\n')) 272 | assert.strictEqual(replyCount, 2) 273 | const str = results[0].toString() 274 | for (let i = 0; i < str.length; i++) { 275 | assert.strictEqual(str.charAt(i), 'a') 276 | } 277 | }) 278 | 279 | it('chunks getting to big for the bufferPool', function () { 280 | // This is a edge case. Chunks should not exceed Math.pow(2, 16) bytes 281 | const lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ' + 282 | 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' + 283 | 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ' + 284 | 'ut aliquip ex ea commodo consequat. Duis aute irure dolor in' // 256 chars 285 | const bigString = (new Array(Math.pow(2, 17) / lorem.length + 1).join(lorem)) // Math.pow(2, 17) chars long 286 | const sizes = [4, Math.pow(2, 17)] 287 | function checkReply (reply) { 288 | assert.strictEqual(reply.length, sizes[replyCount]) 289 | replyCount++ 290 | } 291 | const parser = newParser(checkReply) 292 | parser.execute(Buffer.from('+test')) 293 | assert.strictEqual(replyCount, 0) 294 | parser.execute(Buffer.from('\r\n+')) 295 | assert.strictEqual(replyCount, 1) 296 | parser.execute(Buffer.from(bigString)) 297 | assert.strictEqual(replyCount, 1) 298 | parser.execute(Buffer.from('\r\n')) 299 | assert.strictEqual(replyCount, 2) 300 | }) 301 | 302 | it('handles multi-bulk reply and check context binding', function () { 303 | function Abc () {} 304 | Abc.prototype.checkReply = function (reply) { 305 | assert.strictEqual(typeof this.log, 'function') 306 | assert.deepEqual(reply, [['a']], 'Expecting multi-bulk reply of [["a"]]') 307 | replyCount++ 308 | } 309 | Abc.prototype.log = console.log 310 | const test = new Abc() 311 | const parser = newParser({ 312 | returnReply: function (reply) { 313 | test.checkReply(reply) 314 | } 315 | }) 316 | 317 | parser.execute(Buffer.from('*1\r\n*1\r\n$1\r\na\r\n')) 318 | assert.strictEqual(replyCount, 1) 319 | 320 | parser.execute(Buffer.from('*1\r\n*1\r')) 321 | parser.execute(Buffer.from('\n$1\r\na\r\n')) 322 | assert.strictEqual(replyCount, 2) 323 | 324 | parser.execute(Buffer.from('*1\r\n*1\r\n')) 325 | parser.execute(Buffer.from('$1\r\na\r\n')) 326 | 327 | assert.strictEqual(replyCount, 3, 'check reply should have been called three times') 328 | }) 329 | 330 | it('parser error', function () { 331 | function Abc () {} 332 | Abc.prototype.checkReply = function (err) { 333 | assert.strictEqual(typeof this.log, 'function') 334 | assert.strictEqual(err.message, 'Protocol error, got "a" as reply type byte') 335 | assert.strictEqual(err.name, 'ParserError') 336 | assert(err instanceof RedisError) 337 | assert(err instanceof ParserError) 338 | assert(err instanceof Error) 339 | assert(err.offset) 340 | assert(err.buffer) 341 | assert(/\[97,42,49,13,42,49,13,36,49,96,122,97,115,100,13,10,97]/.test(err.buffer)) 342 | assert(/ParserError: Protocol error, got "a" as reply type byte/.test(util.inspect(err))) 343 | replyCount++ 344 | } 345 | Abc.prototype.log = console.log 346 | const test = new Abc() 347 | const parser = newParser({ 348 | returnFatalError: function (err) { 349 | test.checkReply(err) 350 | } 351 | }) 352 | 353 | parser.execute(Buffer.from('a*1\r*1\r$1`zasd\r\na')) 354 | assert.strictEqual(replyCount, 1) 355 | }) 356 | 357 | it('parser error resets the buffer', function () { 358 | let errCount = 0 359 | function checkReply (reply) { 360 | assert.strictEqual(reply.length, 1) 361 | assert(Buffer.isBuffer(reply[0])) 362 | assert.strictEqual(reply[0].toString(), 'CCC') 363 | replyCount++ 364 | } 365 | function checkError (err) { 366 | assert.strictEqual(err.message, 'Protocol error, got "b" as reply type byte') 367 | errCount++ 368 | } 369 | const parser = new Parser({ 370 | returnReply: checkReply, 371 | returnError: checkError, 372 | returnFatalError: checkError, 373 | returnBuffers: true 374 | }) 375 | 376 | // The chunk contains valid data after the protocol error 377 | parser.execute(Buffer.from('*1\r\n+CCC\r\nb$1\r\nz\r\n+abc\r\n')) 378 | assert.strictEqual(replyCount, 1) 379 | assert.strictEqual(errCount, 1) 380 | parser.execute(Buffer.from('*1\r\n+CCC\r\n')) 381 | assert.strictEqual(replyCount, 2) 382 | parser.execute(Buffer.from('-Protocol error, got "b" as reply type byte\r\n')) 383 | assert.strictEqual(errCount, 2) 384 | }) 385 | 386 | it('parser error v3 without returnFatalError specified', function () { 387 | let errCount = 0 388 | function checkReply (reply) { 389 | assert.strictEqual(reply[0], 'OK') 390 | replyCount++ 391 | } 392 | function checkError (err) { 393 | assert.strictEqual(err.message, 'Protocol error, got "\\n" as reply type byte') 394 | errCount++ 395 | } 396 | const parser = new Parser({ 397 | returnReply: checkReply, 398 | returnError: checkError 399 | }) 400 | 401 | parser.execute(Buffer.from('*1\r\n+OK\r\n\n+zasd\r\n')) 402 | assert.strictEqual(replyCount, 1) 403 | assert.strictEqual(errCount, 1) 404 | }) 405 | 406 | it('should handle \\r and \\n characters properly', function () { 407 | // If a string contains \r or \n characters it will always be send as a bulk string 408 | const entries = ['foo\r', 'foo\r\nbar', '\r\nСанкт-Пет', 'foo\r\n', 'foo', 'foobar', 'foo\r', 'äfooöü', 'abc'] 409 | function checkReply (reply) { 410 | assert.strictEqual(reply, entries[replyCount]) 411 | replyCount++ 412 | } 413 | const parser = newParser(checkReply) 414 | 415 | parser.execute(Buffer.from('$4\r\nfoo\r\r\n$8\r\nfoo\r\nbar\r\n$19\r\n\r\n')) 416 | parser.execute(Buffer.from([208, 161, 208, 176, 208, 189, 208])) 417 | parser.execute(Buffer.from([186, 209, 130, 45, 208, 159, 208, 181, 209, 130])) 418 | assert.strictEqual(replyCount, 2) 419 | parser.execute(Buffer.from('\r\n$5\r\nfoo\r\n\r\n')) 420 | assert.strictEqual(replyCount, 4) 421 | parser.execute(Buffer.from('+foo\r')) 422 | assert.strictEqual(replyCount, 4) 423 | parser.execute(Buffer.from('\n$6\r\nfoobar\r')) 424 | assert.strictEqual(replyCount, 5) 425 | parser.execute(Buffer.from('\n$4\r\nfoo\r\r\n')) 426 | assert.strictEqual(replyCount, 7) 427 | parser.execute(Buffer.from('$9\r\näfo')) 428 | parser.execute(Buffer.from('oö')) 429 | parser.execute(Buffer.from('ü\r')) 430 | assert.strictEqual(replyCount, 7) 431 | parser.execute(Buffer.from('\n+abc\r\n')) 432 | assert.strictEqual(replyCount, 9) 433 | }) 434 | 435 | it('line breaks in the beginning of the last chunk', function () { 436 | function checkReply (reply) { 437 | assert.deepEqual(reply, [['a']], 'Expecting multi-bulk reply of [["a"]]') 438 | replyCount++ 439 | } 440 | const parser = newParser(checkReply) 441 | 442 | parser.execute(Buffer.from('*1\r\n*1\r\n$1\r\na')) 443 | assert.strictEqual(replyCount, 0) 444 | 445 | parser.execute(Buffer.from('\r\n*1\r\n*1\r')) 446 | assert.strictEqual(replyCount, 1) 447 | parser.execute(Buffer.from('\n$1\r\na\r\n*1\r\n*1\r\n$1\r\na\r\n')) 448 | 449 | assert.strictEqual(replyCount, 3, 'check reply should have been called three times') 450 | }) 451 | 452 | it('multiple chunks in a bulk string', function () { 453 | function checkReply (reply) { 454 | assert.strictEqual(reply, 'abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij') 455 | replyCount++ 456 | } 457 | const parser = newParser(checkReply) 458 | 459 | parser.execute(Buffer.from('$100\r\nabcdefghij')) 460 | parser.execute(Buffer.from('abcdefghijabcdefghijabcdefghij')) 461 | parser.execute(Buffer.from('abcdefghijabcdefghijabcdefghij')) 462 | parser.execute(Buffer.from('abcdefghijabcdefghijabcdefghij')) 463 | assert.strictEqual(replyCount, 0) 464 | parser.execute(Buffer.from('\r\n')) 465 | assert.strictEqual(replyCount, 1) 466 | 467 | parser.execute(Buffer.from('$100\r')) 468 | parser.execute(Buffer.from('\nabcdefghijabcdefghijabcdefghijabcdefghij')) 469 | parser.execute(Buffer.from('abcdefghijabcdefghijabcdefghij')) 470 | parser.execute(Buffer.from('abcdefghijabcdefghij')) 471 | assert.strictEqual(replyCount, 1) 472 | parser.execute(Buffer.from( 473 | 'abcdefghij\r\n' + 474 | '$100\r\nabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij\r\n' + 475 | '$100\r\nabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij' 476 | )) 477 | assert.strictEqual(replyCount, 3) 478 | parser.execute(Buffer.from('abcdefghijabcdefghijabcdefghij\r')) 479 | assert.strictEqual(replyCount, 3) 480 | parser.execute(Buffer.from('\n')) 481 | 482 | assert.strictEqual(replyCount, 4, 'check reply should have been called three times') 483 | }) 484 | 485 | it('multiple chunks with arrays different types', function () { 486 | const predefinedData = [ 487 | 'abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij', 488 | 'test', 489 | 100, 490 | new ReplyError('Error message'), 491 | ['The force awakens'], 492 | new ReplyError() 493 | ] 494 | function checkReply (reply) { 495 | for (let i = 0; i < reply.length; i++) { 496 | if (Array.isArray(reply[i])) { 497 | reply[i].forEach(function (reply, j) { 498 | assert.strictEqual(reply, predefinedData[i][j]) 499 | }) 500 | } else if (reply[i] instanceof Error) { 501 | if (Parser.name !== 'HiredisReplyParser') { // The hiredis always returns normal errors in case of nested ones 502 | assert(reply[i] instanceof ReplyError) 503 | assert.strictEqual(reply[i].name, predefinedData[i].name) 504 | } 505 | assert.strictEqual(reply[i].message, predefinedData[i].message) 506 | } else { 507 | assert.strictEqual(reply[i], predefinedData[i]) 508 | } 509 | } 510 | replyCount++ 511 | } 512 | const parser = newParser({ 513 | returnReply: checkReply, 514 | returnBuffers: false 515 | }) 516 | 517 | parser.execute(Buffer.from('*6\r\n$100\r\nabcdefghij')) 518 | parser.execute(Buffer.from('abcdefghijabcdefghijabcdefghij')) 519 | parser.execute(Buffer.from('abcdefghijabcdefghijabcdefghij')) 520 | parser.execute(Buffer.from('abcdefghijabcdefghijabcdefghij\r\n')) 521 | parser.execute(Buffer.from('+test\r')) 522 | parser.execute(Buffer.from('\n:100')) 523 | parser.execute(Buffer.from('\r\n-Error message')) 524 | parser.execute(Buffer.from('\r\n*1\r\n$17\r\nThe force')) 525 | assert.strictEqual(replyCount, 0) 526 | parser.execute(Buffer.from(' awakens\r\n-\r\n$5')) 527 | assert.strictEqual(replyCount, 1) 528 | }) 529 | 530 | it('multiple chunks with nested partial arrays', function () { 531 | const predefinedData = [ 532 | 'abcdefghijabcdefghij', 533 | 100, 534 | '1234567890', 535 | 100 536 | ] 537 | function checkReply (reply) { 538 | assert.strictEqual(reply.length, 1) 539 | for (let i = 0; i < reply[0].length; i++) { 540 | assert.strictEqual(reply[0][i], predefinedData[i]) 541 | } 542 | replyCount++ 543 | } 544 | const parser = newParser({ 545 | returnReply: checkReply 546 | }) 547 | parser.execute(Buffer.from('*1\r\n*4\r\n+abcdefghijabcdefghij\r\n:100')) 548 | parser.execute(Buffer.from('\r\n$10\r\n1234567890\r\n:100')) 549 | assert.strictEqual(replyCount, 0) 550 | parser.execute(Buffer.from('\r\n')) 551 | assert.strictEqual(replyCount, 1) 552 | }) 553 | 554 | it('return normal errors', function () { 555 | function checkReply (reply) { 556 | assert.strictEqual(reply.message, 'Error message') 557 | replyCount++ 558 | } 559 | const parser = newParser({ 560 | returnError: checkReply 561 | }) 562 | 563 | parser.execute(Buffer.from('-Error ')) 564 | parser.execute(Buffer.from('message\r\n*3\r\n$17\r\nThe force')) 565 | assert.strictEqual(replyCount, 1) 566 | parser.execute(Buffer.from(' awakens\r\n$5')) 567 | assert.strictEqual(replyCount, 1) 568 | }) 569 | 570 | it('return null for empty arrays and empty bulk strings', function () { 571 | function checkReply (reply) { 572 | assert.strictEqual(reply, null) 573 | replyCount++ 574 | } 575 | const parser = newParser(checkReply) 576 | 577 | parser.execute(Buffer.from('$-1\r\n*-')) 578 | assert.strictEqual(replyCount, 1) 579 | parser.execute(Buffer.from('1')) 580 | assert.strictEqual(replyCount, 1) 581 | parser.execute(Buffer.from('\r\n$-')) 582 | assert.strictEqual(replyCount, 2) 583 | }) 584 | 585 | it('return value even if all chunks are only 1 character long', function () { 586 | function checkReply (reply) { 587 | assert.strictEqual(reply, 1) 588 | replyCount++ 589 | } 590 | const parser = newParser(checkReply) 591 | 592 | parser.execute(Buffer.from(':')) 593 | assert.strictEqual(replyCount, 0) 594 | parser.execute(Buffer.from('1')) 595 | parser.execute(Buffer.from('\r')) 596 | assert.strictEqual(replyCount, 0) 597 | parser.execute(Buffer.from('\n')) 598 | assert.strictEqual(replyCount, 1) 599 | }) 600 | 601 | it('do not return before \\r\\n', function () { 602 | function checkReply (reply) { 603 | assert.strictEqual(reply, 1) 604 | replyCount++ 605 | } 606 | const parser = newParser(checkReply) 607 | 608 | parser.execute(Buffer.from(':1\r\n:')) 609 | assert.strictEqual(replyCount, 1) 610 | parser.execute(Buffer.from('1')) 611 | assert.strictEqual(replyCount, 1) 612 | parser.execute(Buffer.from('\r')) 613 | assert.strictEqual(replyCount, 1) 614 | parser.execute(Buffer.from('\n')) 615 | assert.strictEqual(replyCount, 2) 616 | }) 617 | 618 | it('return data as buffer if requested', function () { 619 | function checkReply (reply) { 620 | if (Array.isArray(reply)) { 621 | reply = reply[0] 622 | } 623 | assert(Buffer.isBuffer(reply)) 624 | assert.strictEqual(reply.inspect(), Buffer.from('test').inspect()) 625 | replyCount++ 626 | } 627 | const parser = newParser(checkReply, 'buffer') 628 | 629 | parser.execute(Buffer.from('+test\r\n')) 630 | assert.strictEqual(replyCount, 1) 631 | parser.execute(Buffer.from('$4\r\ntest\r')) 632 | parser.execute(Buffer.from('\n')) 633 | assert.strictEqual(replyCount, 2) 634 | parser.execute(Buffer.from('*1\r\n$4\r\nte')) 635 | parser.execute(Buffer.from('st\r')) 636 | parser.execute(Buffer.from('\n')) 637 | assert.strictEqual(replyCount, 3) 638 | }) 639 | 640 | it('handle special case buffer sizes properly', function () { 641 | const entries = ['test test ', 'test test test test ', 1234] 642 | function checkReply (reply) { 643 | assert.strictEqual(reply, entries[replyCount]) 644 | replyCount++ 645 | } 646 | const parser = newParser(checkReply) 647 | parser.execute(Buffer.from('$10\r\ntest ')) 648 | assert.strictEqual(replyCount, 0) 649 | parser.execute(Buffer.from('test \r\n$20\r\ntest test test test \r\n:1234\r')) 650 | assert.strictEqual(replyCount, 2) 651 | parser.execute(Buffer.from('\n')) 652 | assert.strictEqual(replyCount, 3) 653 | }) 654 | 655 | it('return numbers as strings', function () { 656 | if (Parser.name === 'HiredisReplyParser') { 657 | return this.skip() 658 | } 659 | const entries = ['123', '590295810358705700002', '-99999999999999999', '4294967290', '90071992547409920', '10000040000000000000000000000000000000020'] 660 | function checkReply (reply) { 661 | assert.strictEqual(typeof reply, 'string') 662 | assert.strictEqual(reply, entries[replyCount]) 663 | replyCount++ 664 | } 665 | const parser = newParser({ 666 | returnReply: checkReply, 667 | stringNumbers: true 668 | }) 669 | parser.execute(Buffer.from(':123\r\n:590295810358705700002\r\n:-99999999999999999\r\n:4294967290\r\n:90071992547409920\r\n:10000040000000000000000000000000000000020\r\n')) 670 | assert.strictEqual(replyCount, 6) 671 | }) 672 | 673 | it('handle big numbers', function () { 674 | let number = 9007199254740991 // Number.MAX_SAFE_INTEGER 675 | function checkReply (reply) { 676 | assert.strictEqual(reply, number++) 677 | replyCount++ 678 | } 679 | const parser = newParser(checkReply) 680 | parser.execute(Buffer.from(':' + number + '\r\n')) 681 | assert.strictEqual(replyCount, 1) 682 | parser.execute(Buffer.from(':' + number + '\r\n')) 683 | assert.strictEqual(replyCount, 2) 684 | }) 685 | 686 | it('handle big data with buffers', function (done) { 687 | let chunks 688 | const replies = [] 689 | const jsParser = Parser.name === 'JavascriptRedisParser' 690 | function checkReply (reply) { 691 | replies.push(reply) 692 | replyCount++ 693 | } 694 | const parser = newParser(checkReply, 'buffer') 695 | parser.execute(Buffer.from('+test')) 696 | assert.strictEqual(replyCount, 0) 697 | createBufferOfSize(parser, 128 * 1024, '\r\n') 698 | assert.strictEqual(replyCount, 1) 699 | parser.execute(Buffer.from('\r\n')) 700 | assert.strictEqual(replyCount, 2) 701 | setTimeout(function () { 702 | parser.execute(Buffer.from('+test')) 703 | assert.strictEqual(replyCount, 2) 704 | chunks = createBufferOfSize(parser, 256 * 1024, '\r\n') 705 | assert.strictEqual(replyCount, 3) 706 | parser.execute(Buffer.from('\r\n')) 707 | assert.strictEqual(replyCount, 4) 708 | }, 20) 709 | // Delay done so the bufferPool is cleared and tested 710 | // If the buffer is not cleared, the coverage is not going to be at 100 711 | setTimeout(function () { 712 | const totalBuffer = Buffer.concat(chunks).toString() 713 | assert.strictEqual(replies[3].toString(), totalBuffer) 714 | done() 715 | }, (jsParser ? 1400 : 40)) 716 | }) 717 | 718 | it('handle big data', function () { 719 | function checkReply (reply) { 720 | assert.strictEqual(reply.length, 4 * 1024 * 1024) 721 | replyCount++ 722 | } 723 | const parser = newParser(checkReply) 724 | createBufferOfSize(parser, 4 * 1024 * 1024) 725 | assert.strictEqual(replyCount, 0) 726 | parser.execute(Buffer.from('\r\n')) 727 | assert.strictEqual(replyCount, 1) 728 | }) 729 | 730 | it('handle big data 2 with buffers', function (done) { 731 | this.timeout(7500) 732 | const size = 111.5 * 1024 * 1024 733 | const replyLen = [size, size * 2, 11, 11] 734 | function checkReply (reply) { 735 | assert.strictEqual(reply.length, replyLen[replyCount]) 736 | replyCount++ 737 | } 738 | const parser = newParser(checkReply, 'buffer') 739 | createBufferOfSize(parser, size) 740 | assert.strictEqual(replyCount, 0) 741 | createBufferOfSize(parser, size * 2, '\r\n') 742 | assert.strictEqual(replyCount, 1) 743 | parser.execute(Buffer.from('\r\n+hello world')) 744 | assert.strictEqual(replyCount, 2) 745 | parser.execute(Buffer.from('\r\n$11\r\nhuge')) 746 | setTimeout(function () { 747 | parser.execute(Buffer.from(' buffer\r\n')) 748 | assert.strictEqual(replyCount, 4) 749 | done() 750 | }, 60) 751 | }) 752 | }) 753 | }) 754 | }) 755 | --------------------------------------------------------------------------------