Skip to content

Commit 3b7061c

Browse files
author
Ruben Bridgewater
committed
Initial main commit
1 parent 580e4c3 commit 3b7061c

File tree

11 files changed

+815
-29
lines changed

11 files changed

+815
-29
lines changed

.gitignore

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,4 @@
1-
# Logs
21
logs
32
*.log
4-
5-
# Runtime data
6-
pids
7-
*.pid
8-
*.seed
9-
10-
# Directory for instrumented libs generated by jscoverage/JSCover
11-
lib-cov
12-
13-
# Coverage directory used by tools like istanbul
143
coverage
15-
16-
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17-
.grunt
18-
19-
# node-waf configuration
20-
.lock-wscript
21-
22-
# Compiled binary addons (http://nodejs.org/api/addons.html)
23-
build/Release
24-
25-
# Dependency directory
26-
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
274
node_modules

.jshintignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/**
2+
coverage/**
3+
**.md
4+
**.log

.jshintrc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"eqeqeq": true, // Prohibits the use of == and != in favor of === and !==
3+
"noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`
4+
"undef": true, // Require all non-global variables be declared before they are used.
5+
"unused": "vars", // Warn unused variables, but not unused params
6+
"strict": true, // Require `use strict` pragma in every file.
7+
"nonbsp": true, // don't allow non utf-8 pages to break
8+
"forin": true, // don't allow not filtert for in loops
9+
"freeze": true, // prohibit overwriting prototypes of native objects
10+
"maxdepth": 4,
11+
"latedef": true,
12+
"maxparams": 3,
13+
14+
// Environment options
15+
"node": true, // Enable globals available when code is running inside of the NodeJS runtime environment.
16+
"mocha": true,
17+
18+
// Relaxing options
19+
"boss": true // Accept statements like `while (key = keys.pop()) {}`
20+
}

.travis

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
language: node_js
2+
sudo: false
3+
node_js:
4+
- "0.10"
5+
- "0.12"
6+
- "4.0"
7+
- "5.0"

README.md

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,104 @@
1-
# node-redis-parser
2-
Javascript Redis protocol (RESP) parser
1+
[![Build Status](https://travis-ci.org/NodeRedis/redis-parser.png?branch=master)](https://travis-ci.org/NodeRedis/redis-parser)
32

4-
WIP
3+
# redis-parser
4+
5+
A high performance redis parser solution built for [node_redis](https://github.com/NodeRedis/node_redis) and [ioredis](https://github.com/ioredis/luin).
6+
7+
## Install
8+
9+
Install with [NPM](https://npmjs.org/):
10+
11+
```
12+
npm install redis-parser
13+
```
14+
15+
## Usage
16+
17+
```js
18+
new Parser(options);
19+
```
20+
21+
### Possible options
22+
23+
`returnReply`: *function*; mandatory
24+
`returnError`: *function*; mandatory
25+
`returnFatalError`: *function*; optional, defaults to the returnError function
26+
`returnBuffers`: *boolean*; optional, defaults to false
27+
`name`: *javascript|hiredis*; optional, defaults to hiredis and falls back to the js parser if not available
28+
`context`: *A class instance that the return functions get bound to*; optional
29+
30+
### Example
31+
32+
```js
33+
var Parser = require("redis-parser");
34+
35+
function Library () {}
36+
37+
Library.prototype.returnReply = function (reply) { ... }
38+
Library.prototype.returnError = function (err) { ... }
39+
Library.prototype.returnFatalError = function (err) { ... }
40+
41+
var lib = new Library();
42+
43+
var parser = new Parser({
44+
returnReply: returnReply,
45+
returnError: returnError,
46+
returnFatalError: returnFatalError,
47+
context: lib
48+
}); // This returns either a hiredis or the js parser instance depending on what's available
49+
50+
Library.prototype.streamHandler = function () {
51+
this.stream.on('data', function (buffer) {
52+
// Here the data (e.g. `new Buffer('$5\r\nHello\r\n'`)) is passed to the parser and the result is passed to either function depending on the provided data.
53+
// All [RESP](http://redis.io/topics/protocol) data will be properly parsed by the parser.
54+
parser.execute(buffer);
55+
});
56+
};
57+
```
58+
You do not have to use the context variable, but can also bind the function while passing them to the option object.
59+
60+
And if you want to return buffers instead of strings, you can do this by adding the returnBuffers option.
61+
62+
```js
63+
// Same functions as in the first example
64+
65+
var parser = new Parser({
66+
returnReply: returnReply.bind(lib),
67+
returnError: returnError.bind(lib),
68+
returnFatalError: returnFatalError.bind(lib),
69+
returnBuffers: true // All strings are returned as buffer e.g. <Buffer 48 65 6c 6c 6f>
70+
});
71+
72+
// The streamHandler as above
73+
```
74+
75+
## Further info
76+
77+
The [hiredis](https://github.com/redis/hiredis) parser is still the fasted parser for
78+
Node.js and therefor used as default in redis-parser if the hiredis parser is available.
79+
80+
Otherwise the pure js NodeRedis parser is choosen that is almost as fast as the
81+
hiredis parser besides some situations in which it'll be a bit slower.
82+
83+
## Contribute
84+
85+
The js parser is already optimized but there are likely further optimizations possible.
86+
Besides running the tests you'll also have to run the change at least against the node_redis benchmark suite and post the improvement in the PR.
87+
If you want to write a own parser benchmark, that would also be great!
88+
89+
```
90+
npm install
91+
npm test
92+
93+
# Run node_redis benchmark (let's guess you cloned node_redis in another folder)
94+
cd ../redis
95+
npm install
96+
npm run benchmark parser=javascript > old.log
97+
# Replace the changed parser in the node_modules
98+
npm run benchmark parser=javascript > new.log
99+
node benchmarks/diff_multi_bench_output.js old.log new.log > improvement.log
100+
```
101+
102+
## License
103+
104+
[MIT](./LICENSE

index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
'use strict';
2+
3+
module.exports = require('./lib/parser');

lib/hiredis.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict';
2+
3+
var hiredis = require('hiredis');
4+
5+
function HiredisReplyParser(returnBuffers) {
6+
this.name = 'hiredis';
7+
this.reader = new hiredis.Reader({
8+
return_buffers: returnBuffers
9+
});
10+
}
11+
12+
HiredisReplyParser.prototype.parseData = function () {
13+
try {
14+
return this.reader.get();
15+
} catch (err) {
16+
// Protocol errors land here
17+
this.returnFatalError(err);
18+
return void 0;
19+
}
20+
};
21+
22+
HiredisReplyParser.prototype.execute = function (data) {
23+
this.reader.feed(data);
24+
var reply = this.parseData();
25+
26+
while (reply !== undefined) {
27+
if (reply && reply.name === 'Error') {
28+
this.returnError(reply);
29+
} else {
30+
this.returnReply(reply);
31+
}
32+
reply = this.parseData();
33+
}
34+
};
35+
36+
module.exports = HiredisReplyParser;

lib/javascript.js

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
'use strict';
2+
3+
var util = require('util');
4+
5+
function JavascriptReplyParser(return_buffers) {
6+
this.name = 'javascript';
7+
this.buffer = new Buffer(0);
8+
this.offset = 0;
9+
this.bigStrSize = 0;
10+
this.chunksSize = 0;
11+
this.buffers = [];
12+
this.type = 0;
13+
this.protocolError = false;
14+
this.offsetCache = 0;
15+
if (return_buffers) {
16+
this.handleReply = function (start, end) {
17+
return this.buffer.slice(start, end);
18+
};
19+
}
20+
}
21+
22+
JavascriptReplyParser.prototype.handleReply = function (start, end) {
23+
return this.buffer.toString('utf-8', start, end);
24+
};
25+
26+
function IncompleteReadBuffer(message) {
27+
this.name = 'IncompleteReadBuffer';
28+
this.message = message;
29+
}
30+
util.inherits(IncompleteReadBuffer, Error);
31+
32+
JavascriptReplyParser.prototype.parseResult = function (type) {
33+
var start = 0,
34+
end = 0,
35+
packetHeader = 0,
36+
reply;
37+
38+
if (type === 36) { // $
39+
packetHeader = this.parseHeader();
40+
// Packets with a size of -1 are considered null
41+
if (packetHeader === -1) {
42+
return null;
43+
}
44+
end = this.offset + packetHeader;
45+
start = this.offset;
46+
if (end + 2 > this.buffer.length) {
47+
this.buffers.push(this.offsetCache === 0 ? this.buffer : this.buffer.slice(this.offsetCache));
48+
this.chunksSize = this.buffers[0].length;
49+
// Include the packetHeader delimiter
50+
this.bigStrSize = packetHeader + 2;
51+
throw new IncompleteReadBuffer('Wait for more data.');
52+
}
53+
// Set the offset to after the delimiter
54+
this.offset = end + 2;
55+
return this.handleReply(start, end);
56+
} else if (type === 58) { // :
57+
// Up to the delimiter
58+
end = this.packetEndOffset();
59+
start = this.offset;
60+
// Include the delimiter
61+
this.offset = end + 2;
62+
// Return the coerced numeric value
63+
return +this.buffer.toString('ascii', start, end);
64+
} else if (type === 43) { // +
65+
end = this.packetEndOffset();
66+
start = this.offset;
67+
this.offset = end + 2;
68+
return this.handleReply(start, end);
69+
} else if (type === 42) { // *
70+
packetHeader = this.parseHeader();
71+
if (packetHeader === -1) {
72+
return null;
73+
}
74+
reply = [];
75+
for (var i = 0; i < packetHeader; i++) {
76+
if (this.offset >= this.buffer.length) {
77+
throw new IncompleteReadBuffer('Wait for more data.');
78+
}
79+
reply.push(this.parseResult(this.buffer[this.offset++]));
80+
}
81+
return reply;
82+
} else if (type === 45) { // -
83+
end = this.packetEndOffset();
84+
start = this.offset;
85+
this.offset = end + 2;
86+
return new Error(this.buffer.toString('utf-8', start, end));
87+
} else {
88+
return void 0;
89+
}
90+
};
91+
92+
JavascriptReplyParser.prototype.execute = function (buffer) {
93+
if (this.chunksSize !== 0) {
94+
if (this.bigStrSize > this.chunksSize + buffer.length) {
95+
this.buffers.push(buffer);
96+
this.chunksSize += buffer.length;
97+
return;
98+
}
99+
this.buffers.push(buffer);
100+
this.buffer = Buffer.concat(this.buffers, this.chunksSize + buffer.length);
101+
this.buffers = [];
102+
this.bigStrSize = 0;
103+
this.chunksSize = 0;
104+
} else if (this.offset >= this.buffer.length) {
105+
this.buffer = buffer;
106+
} else {
107+
this.buffer = Buffer.concat([this.buffer.slice(this.offset), buffer]);
108+
}
109+
this.offset = 0;
110+
this.protocolError = true;
111+
this.run();
112+
};
113+
114+
JavascriptReplyParser.prototype.tryParsing = function () {
115+
try {
116+
return this.parseResult(this.type);
117+
} catch (err) {
118+
// Catch the error (not enough data), rewind if it's an array,
119+
// and wait for the next packet to appear
120+
this.offset = this.offsetCache;
121+
this.protocolError = false;
122+
return void 0;
123+
}
124+
};
125+
126+
JavascriptReplyParser.prototype.run = function (buffer) {
127+
// Set a rewind point. If a failure occurs, wait for the next execute()/append() and try again
128+
this.offsetCache = this.offset;
129+
this.type = this.buffer[this.offset++];
130+
var reply = this.tryParsing();
131+
132+
while (reply !== undefined) {
133+
if (this.type === 45) { // Errors -
134+
this.returnError(reply);
135+
} else {
136+
this.returnReply(reply); // Strings + // Integers : // Bulk strings $ // Arrays *
137+
}
138+
this.offsetCache = this.offset;
139+
this.type = this.buffer[this.offset++];
140+
reply = this.tryParsing();
141+
}
142+
if (this.type !== undefined && this.protocolError === true) {
143+
// Reset the buffer so the parser can handle following commands properly
144+
this.buffer = new Buffer(0);
145+
this.returnFatalError(new Error('Protocol error, got ' + JSON.stringify(String.fromCharCode(this.type)) + ' as reply type byte'));
146+
}
147+
};
148+
149+
JavascriptReplyParser.prototype.parseHeader = function () {
150+
var end = this.packetEndOffset(),
151+
value = this.buffer.toString('ascii', this.offset, end) | 0;
152+
153+
this.offset = end + 2;
154+
return value;
155+
};
156+
157+
JavascriptReplyParser.prototype.packetEndOffset = function () {
158+
var offset = this.offset,
159+
len = this.buffer.length - 1;
160+
161+
while (this.buffer[offset] !== 0x0d && this.buffer[offset + 1] !== 0x0a) {
162+
offset++;
163+
164+
if (offset >= len) {
165+
throw new IncompleteReadBuffer('Did not see LF after NL reading multi bulk count (' + offset + ' => ' + this.buffer.length + ', ' + this.offset + ')');
166+
}
167+
}
168+
return offset;
169+
};
170+
171+
module.exports = JavascriptReplyParser;

0 commit comments

Comments
 (0)