valkey-parser 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Stalwart Team — Sai Krishna Jamanjyothi (@sunny-7893320220),
4
+ Aditya Sathwik Dasari (@Aditya-sathwik), Chandra Sekhar Kalisetti (@chandu-s-1729),
5
+ Srinu Desetti (@webdevelopersrinu)
6
+ Copyright (c) 2015 NodeRedis (original node-redis-parser)
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # valkey-parser
2
+
3
+ A high performance Javascript parser for the Valkey wire protocol (RESP).
4
+
5
+ This is a Valkey-native port of [`redis-parser`](https://github.com/NodeRedis/node-redis-parser) — same proven, battle-tested parser, with no Redis naming. It uses [`valkey-errors`](https://www.npmjs.com/package/valkey-errors) instead of `redis-errors`.
6
+
7
+ Since Valkey speaks the exact same RESP protocol as Redis, the parsing logic is unchanged.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install valkey-parser
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```js
18
+ const Parser = require('valkey-parser')
19
+
20
+ const parser = new Parser({
21
+ returnReply (reply) { /* ... */ },
22
+ returnError (err) { /* ... */ },
23
+ returnFatalError (err) { /* ... */ },
24
+ returnBuffers: false, // optional
25
+ stringNumbers: false // optional
26
+ })
27
+
28
+ parser.execute(Buffer.from('+OK\r\n'))
29
+ ```
30
+
31
+ ## Options
32
+
33
+ | Option | Description |
34
+ | ------------------ | --------------------------------------------------------------------------- |
35
+ | `returnReply` | *(required)* Called with a parsed reply. |
36
+ | `returnError` | *(required)* Called with a returned `ReplyError`. |
37
+ | `returnFatalError` | *(optional)* Called on a fatal protocol error. Defaults to `returnError`. |
38
+ | `returnBuffers` | *(optional)* Return buffers instead of strings. Default `false`. |
39
+ | `stringNumbers` | *(optional)* Return numbers as strings (for numbers > 2^53). Default `false`. |
40
+
41
+ ## License
42
+
43
+ [MIT](LICENSE) — a fork of `node-redis-parser` (© 2015 NodeRedis).
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ 'use strict'
2
+
3
+ module.exports = require('./lib/parser')
package/lib/parser.js ADDED
@@ -0,0 +1,552 @@
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('valkey-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 {JavascriptValkeyParser} 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 {JavascriptValkeyParser} 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 '+' Valkey simple string response but forward the offsets
82
+ * onto convertBufferRange to generate a string.
83
+ * @param {JavascriptValkeyParser} 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 {JavascriptValkeyParser} 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 ':' Valkey 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 {JavascriptValkeyParser} 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 '$' Valkey bulk string response
142
+ * @param {JavascriptValkeyParser} 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 '-' Valkey error response
170
+ * @param {JavascriptValkeyParser} 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 {JavascriptValkeyParser} 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 '*' Valkey array response
201
+ * @param {JavascriptValkeyParser} 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 {JavascriptValkeyParser} 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 Valkey array response
231
+ * @param {JavascriptValkeyParser} 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 Valkey array response elements
250
+ * @param {JavascriptValkeyParser} 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 {JavascriptValkeyParser} 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 {JavascriptValkeyParser} 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 {JavascriptValkeyParser} 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 JavascriptValkeyParser {
425
+ /**
426
+ * Javascript Valkey 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 Valkey 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 = JavascriptValkeyParser
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "valkey-parser",
3
+ "version": "1.0.0",
4
+ "description": "Javascript Valkey protocol (RESP) parser",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "mocha",
8
+ "benchmark": "node ./benchmark",
9
+ "lint": "standard --fix"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/webdevelopersrinu/valkey-parser.git"
14
+ },
15
+ "keywords": [
16
+ "valkey",
17
+ "protocol",
18
+ "parser",
19
+ "database",
20
+ "javascript",
21
+ "node",
22
+ "nodejs",
23
+ "resp"
24
+ ],
25
+ "files": [
26
+ "index.js",
27
+ "lib"
28
+ ],
29
+ "engines": {
30
+ "node": ">=4"
31
+ },
32
+ "dependencies": {
33
+ "valkey-errors": "^1.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "mocha": "^10.1.0",
37
+ "standard": "^17.0.0"
38
+ },
39
+ "author": "Stalwart Team",
40
+ "license": "MIT",
41
+ "bugs": {
42
+ "url": "https://github.com/webdevelopersrinu/valkey-parser/issues"
43
+ },
44
+ "homepage": "https://github.com/webdevelopersrinu/valkey-parser#readme",
45
+ "directories": {
46
+ "test": "test",
47
+ "lib": "lib"
48
+ }
49
+ }