1 /*global Buffer require exports console setTimeout */
3 // TODO - incorporate these V8 pro tips:
4 // pre-allocate Arrays if length is known in advance
6 // use numbers for parser state
8 var events = require("events"),
9 util = require("../util");
11 exports.debug_mode = false;
12 exports.name = "javascript";
14 function RedisReplyParser(options) {
15 this.name = exports.name;
16 this.options = options || {};
18 events.EventEmitter.call(this);
21 util.inherits(RedisReplyParser, events.EventEmitter);
23 exports.Parser = RedisReplyParser;
25 // Buffer.toString() is quite slow for small strings
26 function small_toString(buf, len) {
29 for (i = 0; i < len; i += 1) {
30 tmp += String.fromCharCode(buf[i]);
36 // Reset parser to it's original state.
37 RedisReplyParser.prototype.reset = function () {
38 this.return_buffer = new Buffer(16384); // for holding replies, might grow
39 this.return_string = "";
40 this.tmp_string = ""; // for holding size fields
42 this.multi_bulk_length = 0;
43 this.multi_bulk_replies = null;
44 this.multi_bulk_pos = 0;
45 this.multi_bulk_nested_length = 0;
46 this.multi_bulk_nested_replies = null;
59 MULTI_BULK_COUNT_LF: 11,
63 this.state = this.states.TYPE;
66 RedisReplyParser.prototype.parser_error = function (message) {
67 this.emit("error", message);
71 RedisReplyParser.prototype.execute = function (incoming_buf) {
72 var pos = 0, bd_tmp, bd_str, i, il, states = this.states;
73 //, state_times = {}, start_execute = new Date(), start_switch, end_switch, old_state;
74 //start_switch = new Date();
76 while (pos < incoming_buf.length) {
77 // old_state = this.state;
78 // console.log("execute: " + this.state + ", " + pos + "/" + incoming_buf.length + ", " + String.fromCharCode(incoming_buf[pos]));
81 case 1: // states.TYPE
82 this.type = incoming_buf[pos];
87 this.state = states.SINGLE_LINE;
88 this.return_buffer.end = 0;
89 this.return_string = "";
92 this.state = states.MULTI_BULK_COUNT;
96 this.state = states.INTEGER_LINE;
97 this.return_buffer.end = 0;
98 this.return_string = "";
101 this.state = states.BULK_LENGTH;
102 this.tmp_string = "";
105 this.state = states.ERROR_LINE;
106 this.return_buffer.end = 0;
107 this.return_string = "";
110 this.state = states.UNKNOWN_TYPE;
113 case 4: // states.INTEGER_LINE
114 if (incoming_buf[pos] === 13) {
115 this.send_reply(+small_toString(this.return_buffer, this.return_buffer.end));
116 this.state = states.FINAL_LF;
118 this.return_buffer[this.return_buffer.end] = incoming_buf[pos];
119 this.return_buffer.end += 1;
123 case 6: // states.ERROR_LINE
124 if (incoming_buf[pos] === 13) {
125 this.send_error(this.return_buffer.toString("ascii", 0, this.return_buffer.end));
126 this.state = states.FINAL_LF;
128 this.return_buffer[this.return_buffer.end] = incoming_buf[pos];
129 this.return_buffer.end += 1;
133 case 2: // states.SINGLE_LINE
134 if (incoming_buf[pos] === 13) {
135 this.send_reply(this.return_string);
136 this.state = states.FINAL_LF;
138 this.return_string += String.fromCharCode(incoming_buf[pos]);
142 case 3: // states.MULTI_BULK_COUNT
143 if (incoming_buf[pos] === 13) { // \r
144 this.state = states.MULTI_BULK_COUNT_LF;
146 this.tmp_string += String.fromCharCode(incoming_buf[pos]);
150 case 11: // states.MULTI_BULK_COUNT_LF
151 if (incoming_buf[pos] === 10) { // \n
152 if (this.multi_bulk_length) { // nested multi-bulk
153 this.multi_bulk_nested_length = this.multi_bulk_length;
154 this.multi_bulk_nested_replies = this.multi_bulk_replies;
155 this.multi_bulk_nested_pos = this.multi_bulk_pos;
157 this.multi_bulk_length = +this.tmp_string;
158 this.multi_bulk_pos = 0;
159 this.state = states.TYPE;
160 if (this.multi_bulk_length < 0) {
161 this.send_reply(null);
162 this.multi_bulk_length = 0;
163 } else if (this.multi_bulk_length === 0) {
164 this.multi_bulk_pos = 0;
165 this.multi_bulk_replies = null;
168 this.multi_bulk_replies = new Array(this.multi_bulk_length);
171 this.parser_error(new Error("didn't see LF after NL reading multi bulk count"));
176 case 5: // states.BULK_LENGTH
177 if (incoming_buf[pos] === 13) { // \r
178 this.state = states.BULK_LF;
180 this.tmp_string += String.fromCharCode(incoming_buf[pos]);
184 case 12: // states.BULK_LF
185 if (incoming_buf[pos] === 10) { // \n
186 this.bulk_length = +this.tmp_string;
187 if (this.bulk_length === -1) {
188 this.send_reply(null);
189 this.state = states.TYPE;
190 } else if (this.bulk_length === 0) {
191 this.send_reply(new Buffer(""));
192 this.state = states.FINAL_CR;
194 this.state = states.BULK_DATA;
195 if (this.bulk_length > this.return_buffer.length) {
196 if (exports.debug_mode) {
197 console.log("Growing return_buffer from " + this.return_buffer.length + " to " + this.bulk_length);
199 this.return_buffer = new Buffer(this.bulk_length);
201 this.return_buffer.end = 0;
204 this.parser_error(new Error("didn't see LF after NL while reading bulk length"));
209 case 7: // states.BULK_DATA
210 this.return_buffer[this.return_buffer.end] = incoming_buf[pos];
211 this.return_buffer.end += 1;
213 if (this.return_buffer.end === this.bulk_length) {
214 bd_tmp = new Buffer(this.bulk_length);
215 // When the response is small, Buffer.copy() is a lot slower.
216 if (this.bulk_length > 10) {
217 this.return_buffer.copy(bd_tmp, 0, 0, this.bulk_length);
219 for (i = 0, il = this.bulk_length; i < il; i += 1) {
220 bd_tmp[i] = this.return_buffer[i];
223 this.send_reply(bd_tmp);
224 this.state = states.FINAL_CR;
227 case 9: // states.FINAL_CR
228 if (incoming_buf[pos] === 13) { // \r
229 this.state = states.FINAL_LF;
232 this.parser_error(new Error("saw " + incoming_buf[pos] + " when expecting final CR"));
236 case 10: // states.FINAL_LF
237 if (incoming_buf[pos] === 10) { // \n
238 this.state = states.TYPE;
241 this.parser_error(new Error("saw " + incoming_buf[pos] + " when expecting final LF"));
246 this.parser_error(new Error("invalid state " + this.state));
248 // end_switch = new Date();
249 // if (state_times[old_state] === undefined) {
250 // state_times[old_state] = 0;
252 // state_times[old_state] += (end_switch - start_switch);
253 // start_switch = end_switch;
255 // console.log("execute ran for " + (Date.now() - start_execute) + " ms, on " + incoming_buf.length + " Bytes. ");
256 // Object.keys(state_times).forEach(function (state) {
257 // console.log(" " + state + ": " + state_times[state]);
261 RedisReplyParser.prototype.send_error = function (reply) {
262 if (this.multi_bulk_length > 0 || this.multi_bulk_nested_length > 0) {
263 // TODO - can this happen? Seems like maybe not.
264 this.add_multi_bulk_reply(reply);
266 this.emit("reply error", reply);
270 RedisReplyParser.prototype.send_reply = function (reply) {
271 if (this.multi_bulk_length > 0 || this.multi_bulk_nested_length > 0) {
272 if (!this.options.return_buffers && Buffer.isBuffer(reply)) {
273 this.add_multi_bulk_reply(reply.toString("utf8"));
275 this.add_multi_bulk_reply(reply);
278 if (!this.options.return_buffers && Buffer.isBuffer(reply)) {
279 this.emit("reply", reply.toString("utf8"));
281 this.emit("reply", reply);
286 RedisReplyParser.prototype.add_multi_bulk_reply = function (reply) {
287 if (this.multi_bulk_replies) {
288 this.multi_bulk_replies[this.multi_bulk_pos] = reply;
289 this.multi_bulk_pos += 1;
290 if (this.multi_bulk_pos < this.multi_bulk_length) {
294 this.multi_bulk_replies = reply;
297 if (this.multi_bulk_nested_length > 0) {
298 this.multi_bulk_nested_replies[this.multi_bulk_nested_pos] = this.multi_bulk_replies;
299 this.multi_bulk_nested_pos += 1;
301 this.multi_bulk_length = 0;
302 this.multi_bulk_replies = null;
303 this.multi_bulk_pos = 0;
305 if (this.multi_bulk_nested_length === this.multi_bulk_nested_pos) {
306 this.emit("reply", this.multi_bulk_nested_replies);
307 this.multi_bulk_nested_length = 0;
308 this.multi_bulk_nested_pos = 0;
309 this.multi_bulk_nested_replies = null;
312 this.emit("reply", this.multi_bulk_replies);
313 this.multi_bulk_length = 0;
314 this.multi_bulk_replies = null;
315 this.multi_bulk_pos = 0;