resplite 1.2.8 → 1.2.12

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/README.md CHANGED
@@ -515,21 +515,24 @@ A typical comparison is **Redis (for example, in Docker)** on one side and **RES
515
515
 
516
516
  **Example results (Redis vs RESPLite, default pragma, 10k iterations):**
517
517
 
518
- | Suite | Redis (Docker) | RESPLite (default) |
519
- |-----------------|----------------|--------------------|
520
- | PING | 8.79K/s | 37.36K/s |
521
- | SET+GET | 4.68K/s | 11.96K/s |
522
- | MSET+MGET(10) | 4.41K/s | 5.81K/s |
523
- | INCR | 9.54K/s | 18.97K/s |
524
- | HSET+HGET | 4.40K/s | 11.91K/s |
525
- | HGETALL(50) | 8.39K/s | 11.01K/s |
526
- | HLEN(50) | 9.36K/s | 31.21K/s |
527
- | SADD+SMEMBERS | 9.27K/s | 17.37K/s |
528
- | LPUSH+LRANGE | 8.34K/s | 14.27K/s |
529
- | LREM | 4.37K/s | 6.08K/s |
530
- | ZADD+ZRANGE | 7.80K/s | 17.12K/s |
531
- | SET+DEL | 4.39K/s | 9.57K/s |
532
- | FT.SEARCH | 8.36K/s | 8.22K/s |
518
+ | Suite | Redis (Docker) | RESPLite (default) |
519
+ |-------------------|----------------|--------------------|
520
+ | PING | 9.72K/s | 37.66K/s |
521
+ | SET+GET | 4.60K/s | 11.96K/s |
522
+ | MSET+MGET(10) | 4.38K/s | 5.71K/s |
523
+ | INCR | 9.76K/s | 19.15K/s |
524
+ | HSET+HGET | 4.42K/s | 11.71K/s |
525
+ | HGETALL(50) | 8.42K/s | 11.12K/s |
526
+ | HLEN(50) | 8.88K/s | 30.72K/s |
527
+ | SADD+SMEMBERS | 8.33K/s | 18.19K/s |
528
+ | LPUSH+LRANGE | 8.29K/s | 14.78K/s |
529
+ | LREM | 4.85K/s | 6.35K/s |
530
+ | ZADD+ZRANGE | 9.37K/s | 16.43K/s |
531
+ | ZADD+ZREVRANGE | 8.22K/s | 16.82K/s |
532
+ | ZRANK+ZREVRANK | 4.56K/s | 13.03K/s |
533
+ | ZREVRANGEBYSCORE | 8.88K/s | 16.88K/s |
534
+ | SET+DEL | 4.75K/s | 9.99K/s |
535
+ | FT.SEARCH | 8.39K/s | 8.81K/s |
533
536
 
534
537
  To reproduce the benchmark, run `npm run benchmark -- --template default`. Numbers depend on host and whether Redis is native or in Docker.
535
538
 
@@ -545,7 +548,7 @@ To reproduce the benchmark, run `npm run benchmark -- --template default`. Numbe
545
548
  | **Hashes** | HSET, HGET, HMGET, HGETALL, HDEL, HEXISTS, HINCRBY |
546
549
  | **Sets** | SADD, SREM, SMEMBERS, SISMEMBER, SCARD |
547
550
  | **Lists** | LPUSH, RPUSH, LLEN, LRANGE, LINDEX, LPOP, RPOP, BLPOP, BRPOP |
548
- | **Sorted sets** | ZADD, ZREM, ZCARD, ZSCORE, ZRANGE, ZRANGEBYSCORE |
551
+ | **Sorted sets** | ZADD, ZREM, ZCARD, ZSCORE, ZRANGE, ZREVRANGE, ZRANGEBYSCORE, ZREVRANGEBYSCORE, ZRANK, ZREVRANK |
549
552
  | **Search (FT.\*)** | FT.CREATE, FT.INFO, FT.ADD, FT.DEL, FT.SEARCH, FT.SUGADD, FT.SUGGET, FT.SUGDEL |
550
553
  | **Introspection** | TYPE, OBJECT IDLETIME, SCAN, KEYS, MONITOR |
551
554
  | **Admin** | SQLITE.INFO, CACHE.INFO, MEMORY.INFO |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resplite",
3
- "version": "1.2.8",
3
+ "version": "1.2.12",
4
4
  "description": "A RESP2 server with practical Redis compatibility, backed by SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -258,6 +258,34 @@ async function benchZaddZrange(client, n) {
258
258
  }
259
259
  }
260
260
 
261
+ async function benchZaddZrevrange(client, n) {
262
+ const key = 'bm:zset:rev';
263
+ for (let i = 0; i < n; i++) {
264
+ await client.zAdd(key, { score: i, value: `m${i}` });
265
+ if (i % 10 === 0) await client.zRange(key, 0, 49, { REV: true });
266
+ }
267
+ }
268
+
269
+ async function benchZrankZrevrank(client, n) {
270
+ const key = 'bm:zset:rank';
271
+ await client.del(key);
272
+ await client.zAdd(key, Array.from({ length: 100 }, (_, i) => ({ score: i, value: `m${i}` })));
273
+ for (let i = 0; i < n; i++) {
274
+ const member = `m${i % 100}`;
275
+ await client.zRank(key, member);
276
+ await client.zRevRank(key, member);
277
+ }
278
+ }
279
+
280
+ async function benchZrevrangebyscore(client, n) {
281
+ const key = 'bm:zset:byscore';
282
+ await client.del(key);
283
+ await client.zAdd(key, Array.from({ length: 100 }, (_, i) => ({ score: i, value: `m${i}` })));
284
+ for (let i = 0; i < n; i++) {
285
+ await client.sendCommand(['ZREVRANGEBYSCORE', key, '99', '0', 'LIMIT', '0', '20']);
286
+ }
287
+ }
288
+
261
289
  async function benchDel(client, n) {
262
290
  for (let i = 0; i < n; i++) {
263
291
  await client.set(`bm:del:${i}`, 'x');
@@ -311,6 +339,9 @@ const SUITES = [
311
339
  { name: 'LPUSH+LRANGE', fn: benchLpushLrange, iterScale: 1 },
312
340
  { name: 'LREM', fn: benchLrem, iterScale: 1 },
313
341
  { name: 'ZADD+ZRANGE', fn: benchZaddZrange, iterScale: 1 },
342
+ { name: 'ZADD+ZREVRANGE', fn: benchZaddZrevrange, iterScale: 1 },
343
+ { name: 'ZRANK+ZREVRANK', fn: benchZrankZrevrank, iterScale: 1 },
344
+ { name: 'ZREVRANGEBYSCORE', fn: benchZrevrangebyscore, iterScale: 1 },
314
345
  { name: 'SET+DEL', fn: benchDel, iterScale: 1 },
315
346
  { name: 'FT.SEARCH', fn: benchFtSearch, iterScale: 1 },
316
347
  ];
@@ -54,6 +54,10 @@ import * as zcard from './zcard.js';
54
54
  import * as zscore from './zscore.js';
55
55
  import * as zrange from './zrange.js';
56
56
  import * as zrangebyscore from './zrangebyscore.js';
57
+ import * as zrevrange from './zrevrange.js';
58
+ import * as zrevrangebyscore from './zrevrangebyscore.js';
59
+ import * as zrevrank from './zrevrank.js';
60
+ import * as zrank from './zrank.js';
57
61
  import * as sqliteInfo from './sqlite-info.js';
58
62
  import * as cacheInfo from './cache-info.js';
59
63
  import * as memoryInfo from './memory-info.js';
@@ -119,6 +123,10 @@ const HANDLERS = new Map([
119
123
  ['ZSCORE', (e, a) => zscore.handleZscore(e, a)],
120
124
  ['ZRANGE', (e, a) => zrange.handleZrange(e, a)],
121
125
  ['ZRANGEBYSCORE', (e, a) => zrangebyscore.handleZrangebyscore(e, a)],
126
+ ['ZREVRANGE', (e, a) => zrevrange.handleZrevrange(e, a)],
127
+ ['ZREVRANGEBYSCORE', (e, a) => zrevrangebyscore.handleZrevrangebyscore(e, a)],
128
+ ['ZREVRANK', (e, a) => zrevrank.handleZrevrank(e, a)],
129
+ ['ZRANK', (e, a) => zrank.handleZrank(e, a)],
122
130
  ['SQLITE.INFO', (e, a) => sqliteInfo.handleSqliteInfo(e, a)],
123
131
  ['CACHE.INFO', (e, a) => cacheInfo.handleCacheInfo(e, a)],
124
132
  ['MEMORY.INFO', (e, a) => memoryInfo.handleMemoryInfo(e, a)],
@@ -0,0 +1,18 @@
1
+ /**
2
+ * ZRANK key member
3
+ * Returns the 0-based rank of member in the sorted set (low to high score).
4
+ * Returns null if key does not exist or member is not in the set.
5
+ */
6
+
7
+ export function handleZrank(engine, args) {
8
+ if (!args || args.length < 2) {
9
+ return { error: 'ERR wrong number of arguments for \'ZRANK\' command' };
10
+ }
11
+ try {
12
+ const rank = engine.zrank(args[0], args[1]);
13
+ return rank;
14
+ } catch (e) {
15
+ const msg = e && e.message ? e.message : String(e);
16
+ return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
17
+ }
18
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * ZREVRANGE key start stop [WITHSCORES]
3
+ * Same as ZRANGE but ordered from highest to lowest score (rank 0 = highest).
4
+ */
5
+
6
+ export function handleZrevrange(engine, args) {
7
+ if (!args || args.length < 3) {
8
+ return { error: 'ERR wrong number of arguments for \'ZREVRANGE\' command' };
9
+ }
10
+ let withScores = false;
11
+ for (let i = 3; i < args.length; i++) {
12
+ const a = (Buffer.isBuffer(args[i]) ? args[i].toString('utf8') : String(args[i])).toUpperCase();
13
+ if (a === 'WITHSCORES') withScores = true;
14
+ }
15
+ try {
16
+ const start = parseInt(String(args[1]), 10);
17
+ const stop = parseInt(String(args[2]), 10);
18
+ if (Number.isNaN(start) || Number.isNaN(stop)) {
19
+ return { error: 'ERR value is not an integer or out of range' };
20
+ }
21
+ const arr = engine.zrevrange(args[0], start, stop, { withScores });
22
+ return arr;
23
+ } catch (e) {
24
+ const msg = e && e.message ? e.message : String(e);
25
+ return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
26
+ }
27
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
3
+ * Same as ZRANGEBYSCORE but ordered from highest to lowest score.
4
+ * Note: Redis uses max min (first score is upper bound, second is lower bound).
5
+ */
6
+
7
+ export function handleZrevrangebyscore(engine, args) {
8
+ if (!args || args.length < 3) {
9
+ return { error: 'ERR wrong number of arguments for \'ZREVRANGEBYSCORE\' command' };
10
+ }
11
+ let withScores = false;
12
+ let limitOffset = null;
13
+ let limitCount = null;
14
+ const raw = args.slice(3);
15
+ for (let i = 0; i < raw.length; i++) {
16
+ const a = (Buffer.isBuffer(raw[i]) ? raw[i].toString('utf8') : String(raw[i])).toUpperCase();
17
+ if (a === 'WITHSCORES') {
18
+ withScores = true;
19
+ } else if (a === 'LIMIT' && i + 2 < raw.length) {
20
+ const off = parseInt(String(raw[i + 1]), 10);
21
+ const cnt = parseInt(String(raw[i + 2]), 10);
22
+ if (!Number.isNaN(off) && !Number.isNaN(cnt)) {
23
+ limitOffset = off;
24
+ limitCount = cnt;
25
+ }
26
+ i += 2;
27
+ }
28
+ }
29
+ try {
30
+ const max = parseFloat(String(args[1]));
31
+ const min = parseFloat(String(args[2]));
32
+ if (Number.isNaN(max) || Number.isNaN(min)) {
33
+ return { error: 'ERR value is not a valid float' };
34
+ }
35
+ const options = { withScores };
36
+ if (limitOffset != null && limitCount != null) {
37
+ options.offset = limitOffset;
38
+ options.limit = limitCount;
39
+ }
40
+ const arr = engine.zrevrangebyscore(args[0], max, min, options);
41
+ return arr;
42
+ } catch (e) {
43
+ const msg = e && e.message ? e.message : String(e);
44
+ return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
45
+ }
46
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * ZREVRANK key member
3
+ * Returns the 0-based rank of member in the sorted set (high to low score).
4
+ * Returns null if key does not exist or member is not in the set.
5
+ */
6
+
7
+ export function handleZrevrank(engine, args) {
8
+ if (!args || args.length < 2) {
9
+ return { error: 'ERR wrong number of arguments for \'ZREVRANK\' command' };
10
+ }
11
+ try {
12
+ const rank = engine.zrevrank(args[0], args[1]);
13
+ return rank;
14
+ } catch (e) {
15
+ const msg = e && e.message ? e.message : String(e);
16
+ return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
17
+ }
18
+ }
@@ -387,6 +387,30 @@ export function createEngine(opts = {}) {
387
387
  return zsets.rangeByRank(k, start, stop, { withScores: options.withScores });
388
388
  },
389
389
 
390
+ zrevrange(key, start, stop, options = {}) {
391
+ const k = asKey(key);
392
+ const meta = getKeyMeta(key);
393
+ if (!meta) return [];
394
+ expectZset(meta);
395
+ return zsets.rangeByRankReverse(k, start, stop, { withScores: options.withScores });
396
+ },
397
+
398
+ zrevrank(key, member) {
399
+ const k = asKey(key);
400
+ const meta = getKeyMeta(key);
401
+ if (!meta) return null;
402
+ expectZset(meta);
403
+ return zsets.rankReverse(k, asKey(member));
404
+ },
405
+
406
+ zrank(key, member) {
407
+ const k = asKey(key);
408
+ const meta = getKeyMeta(key);
409
+ if (!meta) return null;
410
+ expectZset(meta);
411
+ return zsets.rank(k, asKey(member));
412
+ },
413
+
390
414
  zrangebyscore(key, min, max, options = {}) {
391
415
  const k = asKey(key);
392
416
  const meta = getKeyMeta(key);
@@ -399,6 +423,18 @@ export function createEngine(opts = {}) {
399
423
  });
400
424
  },
401
425
 
426
+ zrevrangebyscore(key, max, min, options = {}) {
427
+ const k = asKey(key);
428
+ const meta = getKeyMeta(key);
429
+ if (!meta) return [];
430
+ expectZset(meta);
431
+ return zsets.rangeByScoreReverse(k, max, min, {
432
+ withScores: options.withScores,
433
+ offset: options.offset ?? 0,
434
+ limit: options.limit,
435
+ });
436
+ },
437
+
402
438
  type(key) {
403
439
  const meta = getKeyMeta(key);
404
440
  return typeName(meta);
@@ -36,12 +36,32 @@ export function createZsetsStorage(db, keys) {
36
36
  ORDER BY score ASC, member ASC
37
37
  LIMIT ? OFFSET ?`
38
38
  );
39
+ const rangeByRankReverseStmt = db.prepare(
40
+ `SELECT member, score FROM redis_zsets
41
+ WHERE key = ?
42
+ ORDER BY score DESC, member DESC
43
+ LIMIT ? OFFSET ?`
44
+ );
39
45
  const rangeByScoreStmt = db.prepare(
40
46
  `SELECT member, score FROM redis_zsets
41
47
  WHERE key = ? AND score >= ? AND score <= ?
42
48
  ORDER BY score ASC, member ASC
43
49
  LIMIT ? OFFSET ?`
44
50
  );
51
+ const rankReverseStmt = db.prepare(
52
+ `SELECT COUNT(*) AS n FROM redis_zsets
53
+ WHERE key = ? AND (score > ? OR (score = ? AND member > ?))`
54
+ );
55
+ const rankStmt = db.prepare(
56
+ `SELECT COUNT(*) AS n FROM redis_zsets
57
+ WHERE key = ? AND (score < ? OR (score = ? AND member < ?))`
58
+ );
59
+ const rangeByScoreReverseStmt = db.prepare(
60
+ `SELECT member, score FROM redis_zsets
61
+ WHERE key = ? AND score <= ? AND score >= ?
62
+ ORDER BY score DESC, member DESC
63
+ LIMIT ? OFFSET ?`
64
+ );
45
65
 
46
66
  return {
47
67
  /**
@@ -143,6 +163,34 @@ export function createZsetsStorage(db, keys) {
143
163
  return out;
144
164
  },
145
165
 
166
+ /**
167
+ * Range by rank in reverse order (ZREVRANGE). Rank 0 = highest score.
168
+ * Same start/stop semantics as rangeByRank; order is score DESC, member DESC.
169
+ * @param {Buffer} key
170
+ * @param {number} start 0-based inclusive (0 = highest score)
171
+ * @param {number} stop 0-based inclusive
172
+ * @param {{ withScores?: boolean }} options
173
+ * @returns {Buffer[] | Array<Buffer|string>} members or [member, score, ...]
174
+ */
175
+ rangeByRankReverse(key, start, stop, options = {}) {
176
+ const len = this.count(key);
177
+ if (len === 0) return [];
178
+ let s = start >= 0 ? start : Math.max(0, len + start);
179
+ let e = stop >= 0 ? stop : Math.max(0, len + stop);
180
+ if (s > e) return [];
181
+ s = Math.min(s, len - 1);
182
+ e = Math.min(e, len - 1);
183
+ const limit = e - s + 1;
184
+ const offset = s;
185
+ const rows = rangeByRankReverseStmt.all(key, limit, offset);
186
+ if (!options.withScores) return rows.map((r) => r.member);
187
+ const out = [];
188
+ for (const r of rows) {
189
+ out.push(r.member, formatScore(r.score));
190
+ }
191
+ return out;
192
+ },
193
+
146
194
  /**
147
195
  * Range by score (min/max inclusive). Order: score ASC, member ASC.
148
196
  * @param {Buffer} key
@@ -161,5 +209,52 @@ export function createZsetsStorage(db, keys) {
161
209
  }
162
210
  return out;
163
211
  },
212
+
213
+ /**
214
+ * Rank of member in reverse order (ZREVRANK). Rank 0 = highest score.
215
+ * Returns null if key does not exist or member not in set.
216
+ * @param {Buffer} key
217
+ * @param {Buffer} member
218
+ * @returns {number | null} 0-based rank or null
219
+ */
220
+ rankReverse(key, member) {
221
+ const scoreRow = scoreStmt.get(key, member);
222
+ if (scoreRow == null) return null;
223
+ const row = rankReverseStmt.get(key, scoreRow.score, scoreRow.score, member);
224
+ return row ? row.n : 0;
225
+ },
226
+
227
+ /**
228
+ * Rank of member in ascending order (ZRANK). Rank 0 = lowest score.
229
+ * Returns null if key does not exist or member not in set.
230
+ * @param {Buffer} key
231
+ * @param {Buffer} member
232
+ * @returns {number | null} 0-based rank or null
233
+ */
234
+ rank(key, member) {
235
+ const scoreRow = scoreStmt.get(key, member);
236
+ if (scoreRow == null) return null;
237
+ const row = rankStmt.get(key, scoreRow.score, scoreRow.score, member);
238
+ return row ? row.n : 0;
239
+ },
240
+
241
+ /**
242
+ * Range by score in reverse order (ZREVRANGEBYSCORE). max/min inclusive, order score DESC, member DESC.
243
+ * @param {Buffer} key
244
+ * @param {number} max
245
+ * @param {number} min
246
+ * @param {{ withScores?: boolean, limit?: number, offset?: number }} options
247
+ */
248
+ rangeByScoreReverse(key, max, min, options = {}) {
249
+ const limit = options.limit ?? -1;
250
+ const offset = options.offset ?? 0;
251
+ const rows = rangeByScoreReverseStmt.all(key, max, min, limit < 0 ? 1e9 : limit, offset);
252
+ if (!options.withScores) return rows.map((r) => r.member);
253
+ const out = [];
254
+ for (const r of rows) {
255
+ out.push(r.member, formatScore(r.score));
256
+ }
257
+ return out;
258
+ },
164
259
  };
165
260
  }
@@ -51,6 +51,74 @@ describe('ZSET integration', () => {
51
51
  assert.equal(arr[3].toString('utf8'), '2');
52
52
  });
53
53
 
54
+ it('ZREVRANGE returns order highest to lowest and supports WITHSCORES', async () => {
55
+ await sendCommand(port, argv('ZADD', 'zrev', '1', 'a', '2', 'b', '3', 'c'));
56
+ const rev = await sendCommand(port, argv('ZREVRANGE', 'zrev', '0', '-1'));
57
+ const arr = tryParseValue(rev, 0).value;
58
+ assert.equal(arr.length, 3);
59
+ assert.equal(arr[0].toString('utf8'), 'c');
60
+ assert.equal(arr[1].toString('utf8'), 'b');
61
+ assert.equal(arr[2].toString('utf8'), 'a');
62
+
63
+ const withScores = await sendCommand(port, argv('ZREVRANGE', 'zrev', '0', '30', 'WITHSCORES'));
64
+ const withArr = tryParseValue(withScores, 0).value;
65
+ assert.equal(withArr.length, 6);
66
+ assert.equal(withArr[0].toString('utf8'), 'c');
67
+ assert.equal(withArr[1].toString('utf8'), '3');
68
+ assert.equal(withArr[2].toString('utf8'), 'b');
69
+ assert.equal(withArr[3].toString('utf8'), '2');
70
+ });
71
+
72
+ it('ZREVRANK returns 0-based rank high-to-low and nil when missing', async () => {
73
+ await sendCommand(port, argv('ZADD', 'zrevrank', '1', 'a', '2', 'b', '3', 'c'));
74
+ const rankC = await sendCommand(port, argv('ZREVRANK', 'zrevrank', 'c'));
75
+ assert.equal(tryParseValue(rankC, 0).value, 0);
76
+ const rankB = await sendCommand(port, argv('ZREVRANK', 'zrevrank', 'b'));
77
+ assert.equal(tryParseValue(rankB, 0).value, 1);
78
+ const rankA = await sendCommand(port, argv('ZREVRANK', 'zrevrank', 'a'));
79
+ assert.equal(tryParseValue(rankA, 0).value, 2);
80
+
81
+ const noMember = await sendCommand(port, argv('ZREVRANK', 'zrevrank', 'x'));
82
+ assert.ok(noMember.toString('ascii').startsWith('$-1'));
83
+ const noKey = await sendCommand(port, argv('ZREVRANK', 'nokey', 'a'));
84
+ assert.ok(noKey.toString('ascii').startsWith('$-1'));
85
+ });
86
+
87
+ it('ZRANK returns 0-based rank low-to-high and nil when missing', async () => {
88
+ await sendCommand(port, argv('ZADD', 'zrank', '1', 'a', '2', 'b', '3', 'c'));
89
+ const rankA = await sendCommand(port, argv('ZRANK', 'zrank', 'a'));
90
+ assert.equal(tryParseValue(rankA, 0).value, 0);
91
+ const rankB = await sendCommand(port, argv('ZRANK', 'zrank', 'b'));
92
+ assert.equal(tryParseValue(rankB, 0).value, 1);
93
+ const rankC = await sendCommand(port, argv('ZRANK', 'zrank', 'c'));
94
+ assert.equal(tryParseValue(rankC, 0).value, 2);
95
+
96
+ const noMember = await sendCommand(port, argv('ZRANK', 'zrank', 'x'));
97
+ assert.ok(noMember.toString('ascii').startsWith('$-1'));
98
+ });
99
+
100
+ it('ZREVRANGEBYSCORE returns score range high-to-low and supports WITHSCORES and LIMIT', async () => {
101
+ await sendCommand(port, argv('ZADD', 'zrevscore', '1', 'a', '2', 'b', '3', 'c', '4', 'd'));
102
+ const rev = await sendCommand(port, argv('ZREVRANGEBYSCORE', 'zrevscore', '4', '2'));
103
+ const arr = tryParseValue(rev, 0).value;
104
+ assert.equal(arr.length, 3);
105
+ assert.equal(arr[0].toString('utf8'), 'd');
106
+ assert.equal(arr[1].toString('utf8'), 'c');
107
+ assert.equal(arr[2].toString('utf8'), 'b');
108
+
109
+ const withScores = await sendCommand(port, argv('ZREVRANGEBYSCORE', 'zrevscore', '10', '0', 'WITHSCORES'));
110
+ const withArr = tryParseValue(withScores, 0).value;
111
+ assert.equal(withArr.length, 8);
112
+ assert.equal(withArr[0].toString('utf8'), 'd');
113
+ assert.equal(withArr[1].toString('utf8'), '4');
114
+
115
+ const limitReply = await sendCommand(port, argv('ZREVRANGEBYSCORE', 'zrevscore', '10', '0', 'LIMIT', '1', '2'));
116
+ const limited = tryParseValue(limitReply, 0).value;
117
+ assert.equal(limited.length, 2);
118
+ assert.equal(limited[0].toString('utf8'), 'c');
119
+ assert.equal(limited[1].toString('utf8'), 'b');
120
+ });
121
+
54
122
  it('ZRANGE negative indices and start > stop', async () => {
55
123
  await sendCommand(port, argv('ZADD', 'z4', '1', 'a', '2', 'b', '3', 'c'));
56
124
  const last = await sendCommand(port, argv('ZRANGE', 'z4', '-1', '-1'));