resplite 1.2.20 → 1.3.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/README.md CHANGED
@@ -126,11 +126,13 @@ const ks = await m.enableKeyspaceNotifications();
126
126
  // → { ok: true, previous: '', applied: 'KEA' }
127
127
  // If CONFIG is renamed and configCommand was not set, ok=false and error explains how to fix it.
128
128
 
129
- // Step 0c — Start dirty tracking (in-process, same script)
129
+ // Start dirty tracking (in-process, same script)
130
+ let dirtyLogging = true;
130
131
  await m.startDirtyTracker({
131
132
  onProgress: (p) => {
132
- // one callback per keyspace event tracked during bulk/cutover
133
- console.log(`[dirty ${p.totalEvents}] event=${p.event} key=${p.key}`);
133
+ if (dirtyLogging) {
134
+ console.log(`[dirty ${p.totalEvents}] event=${p.event} key=${p.key}`);
135
+ }
134
136
  },
135
137
  });
136
138
 
@@ -151,8 +153,12 @@ await m.bulk({
151
153
  const { run, dirty } = m.status();
152
154
  console.log('bulk status:', run.status, '— dirty counts:', dirty);
153
155
 
156
+ // Stop dirty progress logs so the next prompt is visible (tracker keeps recording until stopDirtyTracker)
157
+ dirtyLogging = false;
158
+
154
159
  // Step 2 — Pause for cutover:
155
160
  // stop the app that is still writing to Redis, then press Enter.
161
+ // (readline reads from stdin, so Enter is captured even if anything else writes to stdout.)
156
162
  const rl = createInterface({ input: stdin, output: stdout });
157
163
  await rl.question('Stop app traffic to Redis, then press Enter to apply the final dirty set...');
158
164
  rl.close();
@@ -543,14 +549,14 @@ To reproduce the benchmark, run `npm run benchmark -- --template default`. Numbe
543
549
  | Category | Commands |
544
550
  |---|---|
545
551
  | **Connection** | PING, ECHO, QUIT |
546
- | **Strings** | GET, SET, MGET, MSET, DEL, EXISTS, INCR, DECR, INCRBY, DECRBY |
552
+ | **Strings** | GET, SET, MGET, MSET, DEL, EXISTS, INCR, DECR, INCRBY, DECRBY, STRLEN |
547
553
  | **TTL** | EXPIRE, PEXPIRE, TTL, PTTL, PERSIST |
548
- | **Hashes** | HSET, HGET, HMGET, HGETALL, HDEL, HEXISTS, HINCRBY |
549
- | **Sets** | SADD, SREM, SMEMBERS, SISMEMBER, SCARD |
550
- | **Lists** | LPUSH, RPUSH, LLEN, LRANGE, LINDEX, LPOP, RPOP, BLPOP, BRPOP |
551
- | **Sorted sets** | ZADD, ZREM, ZCARD, ZSCORE, ZRANGE, ZREVRANGE, ZRANGEBYSCORE, ZREVRANGEBYSCORE, ZRANK, ZREVRANK |
554
+ | **Hashes** | HSET, HGET, HMGET, HGETALL, HKEYS, HVALS, HDEL, HEXISTS, HINCRBY |
555
+ | **Sets** | SADD, SREM, SMEMBERS, SISMEMBER, SCARD, SPOP, SRANDMEMBER |
556
+ | **Lists** | LPUSH, RPUSH, LLEN, LRANGE, LINDEX, LPOP, RPOP, LSET, LTRIM, BLPOP, BRPOP |
557
+ | **Sorted sets** | ZADD, ZREM, ZCARD, ZSCORE, ZRANGE, ZREVRANGE, ZRANGEBYSCORE, ZREVRANGEBYSCORE, ZRANK, ZREVRANK, ZCOUNT, ZINCRBY, ZREMRANGEBYRANK, ZREMRANGEBYSCORE |
552
558
  | **Search (FT.\*)** | FT.CREATE, FT.INFO, FT.ADD, FT.DEL, FT.SEARCH, FT.SUGADD, FT.SUGGET, FT.SUGDEL |
553
- | **Introspection** | TYPE, OBJECT IDLETIME, SCAN, KEYS, MONITOR |
559
+ | **Introspection** | TYPE, OBJECT IDLETIME, SCAN, KEYS, RENAME, MONITOR |
554
560
  | **Admin** | SQLITE.INFO, CACHE.INFO, MEMORY.INFO |
555
561
 
556
562
  ### Not supported (v1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resplite",
3
- "version": "1.2.20",
3
+ "version": "1.3.0",
4
4
  "description": "A RESP2 server with practical Redis compatibility, backed by SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -293,6 +293,103 @@ async function benchDel(client, n) {
293
293
  }
294
294
  }
295
295
 
296
+ async function benchStrlen(client, n) {
297
+ const key = 'bm:strlen';
298
+ await client.set(key, 'hello-world');
299
+ for (let i = 0; i < n; i++) await client.sendCommand(['STRLEN', key]);
300
+ }
301
+
302
+ async function benchHkeys(client, n) {
303
+ const key = 'bm:hash:keys';
304
+ await client.del(key);
305
+ await client.hSet(key, Object.fromEntries(Array.from({ length: 50 }, (_, i) => [`f${i}`, `v${i}`])));
306
+ for (let i = 0; i < n; i++) await client.sendCommand(['HKEYS', key]);
307
+ }
308
+
309
+ async function benchHvals(client, n) {
310
+ const key = 'bm:hash:vals';
311
+ await client.del(key);
312
+ await client.hSet(key, Object.fromEntries(Array.from({ length: 50 }, (_, i) => [`f${i}`, `v${i}`])));
313
+ for (let i = 0; i < n; i++) await client.sendCommand(['HVALS', key]);
314
+ }
315
+
316
+ async function benchLset(client, n) {
317
+ const key = 'bm:list:lset';
318
+ await client.del(key);
319
+ await client.rPush(key, Array.from({ length: 10 }, (_, i) => `item-${i}`));
320
+ for (let i = 0; i < n; i++) await client.sendCommand(['LSET', key, '5', `val-${i}`]);
321
+ }
322
+
323
+ async function benchLtrim(client, n) {
324
+ const key = 'bm:list:ltrim';
325
+ for (let i = 0; i < n; i++) {
326
+ await client.del(key);
327
+ await client.rPush(key, Array.from({ length: 20 }, (_, j) => `x${j}`));
328
+ await client.sendCommand(['LTRIM', key, '0', '9']);
329
+ }
330
+ }
331
+
332
+ async function benchRename(client, n) {
333
+ const a = 'bm:rename:a';
334
+ const b = 'bm:rename:b';
335
+ await client.set(a, 'v');
336
+ for (let i = 0; i < n; i++) {
337
+ await client.sendCommand(['RENAME', a, b]);
338
+ await client.sendCommand(['RENAME', b, a]);
339
+ }
340
+ }
341
+
342
+ async function benchZcount(client, n) {
343
+ const key = 'bm:zset:count';
344
+ await client.del(key);
345
+ await client.zAdd(key, Array.from({ length: 100 }, (_, i) => ({ score: i, value: `m${i}` })));
346
+ for (let i = 0; i < n; i++) await client.sendCommand(['ZCOUNT', key, '0', '99']);
347
+ }
348
+
349
+ async function benchZincrby(client, n) {
350
+ const key = 'bm:zset:incr';
351
+ await client.del(key);
352
+ await client.zAdd(key, { score: 0, value: 'member' });
353
+ for (let i = 0; i < n; i++) await client.sendCommand(['ZINCRBY', key, '1', 'member']);
354
+ }
355
+
356
+ async function benchZremrangebyrank(client, n) {
357
+ const key = 'bm:zset:remrank';
358
+ for (let i = 0; i < n; i++) {
359
+ await client.del(key);
360
+ await client.zAdd(key, Array.from({ length: 100 }, (_, j) => ({ score: j, value: `m${j}` })));
361
+ await client.sendCommand(['ZREMRANGEBYRANK', key, '0', '9']);
362
+ }
363
+ }
364
+
365
+ async function benchZremrangebyscore(client, n) {
366
+ const key = 'bm:zset:remscore';
367
+ for (let i = 0; i < n; i++) {
368
+ await client.del(key);
369
+ await client.zAdd(key, Array.from({ length: 100 }, (_, j) => ({ score: j, value: `m${j}` })));
370
+ await client.sendCommand(['ZREMRANGEBYSCORE', key, '0', '9']);
371
+ }
372
+ }
373
+
374
+ async function benchSpop(client, n) {
375
+ const key = 'bm:set:spop';
376
+ await client.del(key);
377
+ await client.sAdd(key, Array.from({ length: 50 }, (_, i) => `m${i}`));
378
+ for (let i = 0; i < n; i++) {
379
+ const v = await client.sendCommand(['SPOP', key]);
380
+ if (v === null || (Array.isArray(v) && v.length === 0)) {
381
+ await client.sAdd(key, Array.from({ length: 50 }, (_, j) => `m${j}`));
382
+ }
383
+ }
384
+ }
385
+
386
+ async function benchSrandmember(client, n) {
387
+ const key = 'bm:set:srand';
388
+ await client.del(key);
389
+ await client.sAdd(key, Array.from({ length: 50 }, (_, i) => `m${i}`));
390
+ for (let i = 0; i < n; i++) await client.sendCommand(['SRANDMEMBER', key]);
391
+ }
392
+
296
393
  const FT_INDEX = 'bm_ft_idx';
297
394
  const FT_DOCS = 50;
298
395
 
@@ -343,6 +440,18 @@ const SUITES = [
343
440
  { name: 'ZRANK+ZREVRANK', fn: benchZrankZrevrank, iterScale: 1 },
344
441
  { name: 'ZREVRANGEBYSCORE', fn: benchZrevrangebyscore, iterScale: 1 },
345
442
  { name: 'SET+DEL', fn: benchDel, iterScale: 1 },
443
+ { name: 'STRLEN', fn: benchStrlen, iterScale: 1 },
444
+ { name: 'HKEYS(50)', fn: benchHkeys, iterScale: 1 },
445
+ { name: 'HVALS(50)', fn: benchHvals, iterScale: 1 },
446
+ { name: 'LSET', fn: benchLset, iterScale: 1 },
447
+ { name: 'LTRIM', fn: benchLtrim, iterScale: 1 },
448
+ { name: 'RENAME', fn: benchRename, iterScale: 1 },
449
+ { name: 'ZCOUNT', fn: benchZcount, iterScale: 1 },
450
+ { name: 'ZINCRBY', fn: benchZincrby, iterScale: 1 },
451
+ { name: 'ZREMRANGEBYRANK', fn: benchZremrangebyrank, iterScale: 1 },
452
+ { name: 'ZREMRANGEBYSCORE', fn: benchZremrangebyscore, iterScale: 1 },
453
+ { name: 'SPOP', fn: benchSpop, iterScale: 1 },
454
+ { name: 'SRANDMEMBER', fn: benchSrandmember, iterScale: 1 },
346
455
  { name: 'FT.SEARCH', fn: benchFtSearch, iterScale: 1 },
347
456
  ];
348
457
 
@@ -0,0 +1,16 @@
1
+ /**
2
+ * HKEYS key - returns array of field names in the hash.
3
+ */
4
+
5
+ export function handleHkeys(engine, args) {
6
+ if (!args || args.length < 1) {
7
+ return { error: 'ERR wrong number of arguments for \'HKEYS\' command' };
8
+ }
9
+ try {
10
+ const fields = engine.hkeys(args[0]);
11
+ return fields;
12
+ } catch (e) {
13
+ const msg = e && e.message ? e.message : String(e);
14
+ return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * HVALS key - returns array of values in the hash.
3
+ */
4
+
5
+ export function handleHvals(engine, args) {
6
+ if (!args || args.length < 1) {
7
+ return { error: 'ERR wrong number of arguments for \'HVALS\' command' };
8
+ }
9
+ try {
10
+ const values = engine.hvals(args[0]);
11
+ return values;
12
+ } catch (e) {
13
+ const msg = e && e.message ? e.message : String(e);
14
+ return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * LSET key index element - sets list element at index to element. Returns OK.
3
+ */
4
+
5
+ export function handleLset(engine, args) {
6
+ if (!args || args.length < 3) {
7
+ return { error: 'ERR wrong number of arguments for \'LSET\' command' };
8
+ }
9
+ try {
10
+ engine.lset(args[0], args[1], args[2]);
11
+ return { simple: 'OK' };
12
+ } catch (e) {
13
+ const msg = e && e.message ? e.message : String(e);
14
+ return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * LTRIM key start stop - trims list to the specified range. Returns OK.
3
+ */
4
+
5
+ export function handleLtrim(engine, args) {
6
+ if (!args || args.length < 3) {
7
+ return { error: 'ERR wrong number of arguments for \'LTRIM\' command' };
8
+ }
9
+ try {
10
+ engine.ltrim(args[0], args[1], args[2]);
11
+ return { simple: 'OK' };
12
+ } catch (e) {
13
+ const msg = e && e.message ? e.message : String(e);
14
+ return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
15
+ }
16
+ }
@@ -9,6 +9,7 @@ import * as quit from './quit.js';
9
9
  import * as get from './get.js';
10
10
  import * as set from './set.js';
11
11
  import * as setex from './setex.js';
12
+ import * as strlen from './strlen.js';
12
13
  import * as del from './del.js';
13
14
  import * as unlink from './unlink.js';
14
15
  import * as exists from './exists.js';
@@ -29,6 +30,8 @@ import * as hset from './hset.js';
29
30
  import * as hget from './hget.js';
30
31
  import * as hmget from './hmget.js';
31
32
  import * as hgetall from './hgetall.js';
33
+ import * as hkeys from './hkeys.js';
34
+ import * as hvals from './hvals.js';
32
35
  import * as hdel from './hdel.js';
33
36
  import * as hlen from './hlen.js';
34
37
  import * as hexists from './hexists.js';
@@ -38,6 +41,8 @@ import * as srem from './srem.js';
38
41
  import * as smembers from './smembers.js';
39
42
  import * as sismember from './sismember.js';
40
43
  import * as scard from './scard.js';
44
+ import * as spop from './spop.js';
45
+ import * as srandmember from './srandmember.js';
41
46
  import * as lpush from './lpush.js';
42
47
  import * as rpush from './rpush.js';
43
48
  import * as llen from './llen.js';
@@ -46,10 +51,13 @@ import * as lindex from './lindex.js';
46
51
  import * as lpop from './lpop.js';
47
52
  import * as rpop from './rpop.js';
48
53
  import * as lrem from './lrem.js';
54
+ import * as lset from './lset.js';
55
+ import * as ltrim from './ltrim.js';
49
56
  import * as blpop from './blpop.js';
50
57
  import * as brpop from './brpop.js';
51
58
  import * as scan from './scan.js';
52
59
  import * as keys from './keys.js';
60
+ import * as rename from './rename.js';
53
61
  import * as zadd from './zadd.js';
54
62
  import * as zrem from './zrem.js';
55
63
  import * as zcard from './zcard.js';
@@ -60,6 +68,10 @@ import * as zrevrange from './zrevrange.js';
60
68
  import * as zrevrangebyscore from './zrevrangebyscore.js';
61
69
  import * as zrevrank from './zrevrank.js';
62
70
  import * as zrank from './zrank.js';
71
+ import * as zcount from './zcount.js';
72
+ import * as zincrby from './zincrby.js';
73
+ import * as zremrangebyrank from './zremrangebyrank.js';
74
+ import * as zremrangebyscore from './zremrangebyscore.js';
63
75
  import * as sqliteInfo from './sqlite-info.js';
64
76
  import * as cacheInfo from './cache-info.js';
65
77
  import * as memoryInfo from './memory-info.js';
@@ -82,6 +94,7 @@ const HANDLERS = new Map([
82
94
  ['GET', (e, a) => get.handleGet(e, a)],
83
95
  ['SET', (e, a) => set.handleSet(e, a)],
84
96
  ['SETEX', (e, a) => setex.handleSetex(e, a)],
97
+ ['STRLEN', (e, a) => strlen.handleStrlen(e, a)],
85
98
  ['DEL', (e, a) => del.handleDel(e, a)],
86
99
  ['UNLINK', (e, a) => unlink.handleUnlink(e, a)],
87
100
  ['EXISTS', (e, a) => exists.handleExists(e, a)],
@@ -102,6 +115,8 @@ const HANDLERS = new Map([
102
115
  ['HGET', (e, a) => hget.handleHget(e, a)],
103
116
  ['HMGET', (e, a) => hmget.handleHmget(e, a)],
104
117
  ['HGETALL', (e, a) => hgetall.handleHgetall(e, a)],
118
+ ['HKEYS', (e, a) => hkeys.handleHkeys(e, a)],
119
+ ['HVALS', (e, a) => hvals.handleHvals(e, a)],
105
120
  ['HDEL', (e, a) => hdel.handleHdel(e, a)],
106
121
  ['HLEN', (e, a) => hlen.handleHlen(e, a)],
107
122
  ['HEXISTS', (e, a) => hexists.handleHexists(e, a)],
@@ -111,6 +126,8 @@ const HANDLERS = new Map([
111
126
  ['SMEMBERS', (e, a) => smembers.handleSmembers(e, a)],
112
127
  ['SISMEMBER', (e, a) => sismember.handleSismember(e, a)],
113
128
  ['SCARD', (e, a) => scard.handleScard(e, a)],
129
+ ['SPOP', (e, a) => spop.handleSpop(e, a)],
130
+ ['SRANDMEMBER', (e, a) => srandmember.handleSrandmember(e, a)],
114
131
  ['LPUSH', (e, a) => lpush.handleLpush(e, a)],
115
132
  ['RPUSH', (e, a) => rpush.handleRpush(e, a)],
116
133
  ['LLEN', (e, a) => llen.handleLlen(e, a)],
@@ -119,10 +136,13 @@ const HANDLERS = new Map([
119
136
  ['LPOP', (e, a, ctx) => lpop.handleLpop(e, a)],
120
137
  ['RPOP', (e, a, ctx) => rpop.handleRpop(e, a)],
121
138
  ['LREM', (e, a) => lrem.handleLrem(e, a)],
139
+ ['LSET', (e, a) => lset.handleLset(e, a)],
140
+ ['LTRIM', (e, a) => ltrim.handleLtrim(e, a)],
122
141
  ['BLPOP', (e, a, ctx) => blpop.handleBlpop(e, a, ctx)],
123
142
  ['BRPOP', (e, a, ctx) => brpop.handleBrpop(e, a, ctx)],
124
143
  ['SCAN', (e, a) => scan.handleScan(e, a)],
125
144
  ['KEYS', (e, a) => keys.handleKeys(e, a)],
145
+ ['RENAME', (e, a) => rename.handleRename(e, a)],
126
146
  ['ZADD', (e, a) => zadd.handleZadd(e, a)],
127
147
  ['ZREM', (e, a) => zrem.handleZrem(e, a)],
128
148
  ['ZCARD', (e, a) => zcard.handleZcard(e, a)],
@@ -133,6 +153,10 @@ const HANDLERS = new Map([
133
153
  ['ZREVRANGEBYSCORE', (e, a) => zrevrangebyscore.handleZrevrangebyscore(e, a)],
134
154
  ['ZREVRANK', (e, a) => zrevrank.handleZrevrank(e, a)],
135
155
  ['ZRANK', (e, a) => zrank.handleZrank(e, a)],
156
+ ['ZCOUNT', (e, a) => zcount.handleZcount(e, a)],
157
+ ['ZINCRBY', (e, a) => zincrby.handleZincrby(e, a)],
158
+ ['ZREMRANGEBYRANK', (e, a) => zremrangebyrank.handleZremrangebyrank(e, a)],
159
+ ['ZREMRANGEBYSCORE', (e, a) => zremrangebyscore.handleZremrangebyscore(e, a)],
136
160
  ['SQLITE.INFO', (e, a) => sqliteInfo.handleSqliteInfo(e, a)],
137
161
  ['CACHE.INFO', (e, a) => cacheInfo.handleCacheInfo(e, a)],
138
162
  ['MEMORY.INFO', (e, a) => memoryInfo.handleMemoryInfo(e, a)],
@@ -0,0 +1,16 @@
1
+ /**
2
+ * RENAME key newkey - renames key to newkey, overwriting newkey if it exists. Returns OK.
3
+ */
4
+
5
+ export function handleRename(engine, args) {
6
+ if (!args || args.length < 2) {
7
+ return { error: 'ERR wrong number of arguments for \'RENAME\' command' };
8
+ }
9
+ try {
10
+ engine.rename(args[0], args[1]);
11
+ return { simple: 'OK' };
12
+ } catch (e) {
13
+ const msg = e && e.message ? e.message : String(e);
14
+ return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
15
+ }
16
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * SPOP key [count] - removes and returns one or more random members. Default count 1.
3
+ */
4
+
5
+ export function handleSpop(engine, args) {
6
+ if (!args || args.length < 1) {
7
+ return { error: 'ERR wrong number of arguments for \'SPOP\' command' };
8
+ }
9
+ try {
10
+ let count = null;
11
+ if (args.length >= 2) {
12
+ const n = parseInt(String(args[1]), 10);
13
+ if (!Number.isNaN(n)) count = n;
14
+ }
15
+ const result = engine.spop(args[0], count);
16
+ return result;
17
+ } catch (e) {
18
+ const msg = e && e.message ? e.message : String(e);
19
+ return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * SRANDMEMBER key [count] - returns one or more random members without removing. Default count 1.
3
+ */
4
+
5
+ export function handleSrandmember(engine, args) {
6
+ if (!args || args.length < 1) {
7
+ return { error: 'ERR wrong number of arguments for \'SRANDMEMBER\' command' };
8
+ }
9
+ try {
10
+ let count = null;
11
+ if (args.length >= 2) {
12
+ const n = parseInt(String(args[1]), 10);
13
+ if (!Number.isNaN(n)) count = n;
14
+ }
15
+ const result = engine.srandmember(args[0], count);
16
+ return result;
17
+ } catch (e) {
18
+ const msg = e && e.message ? e.message : String(e);
19
+ return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
20
+ }
21
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * STRLEN key - returns length of string value in bytes, or 0 if key does not exist.
3
+ */
4
+
5
+ export function handleStrlen(engine, args) {
6
+ if (!args || args.length < 1) {
7
+ return { error: 'ERR wrong number of arguments for \'STRLEN\' command' };
8
+ }
9
+ try {
10
+ const len = engine.strlen(args[0]);
11
+ return len;
12
+ } catch (e) {
13
+ const msg = e && e.message ? e.message : String(e);
14
+ return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
15
+ }
16
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * ZCOUNT key min max - returns count of members in sorted set with score in [min, max].
3
+ */
4
+
5
+ export function handleZcount(engine, args) {
6
+ if (!args || args.length < 3) {
7
+ return { error: 'ERR wrong number of arguments for \'ZCOUNT\' command' };
8
+ }
9
+ try {
10
+ const min = parseFloat(String(args[1]));
11
+ const max = parseFloat(String(args[2]));
12
+ if (Number.isNaN(min) || Number.isNaN(max)) {
13
+ return { error: 'ERR value is not a valid float' };
14
+ }
15
+ const n = engine.zcount(args[0], min, max);
16
+ return n;
17
+ } catch (e) {
18
+ const msg = e && e.message ? e.message : String(e);
19
+ return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
20
+ }
21
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * ZINCRBY key increment member - increments member's score, returns new score.
3
+ */
4
+
5
+ export function handleZincrby(engine, args) {
6
+ if (!args || args.length < 3) {
7
+ return { error: 'ERR wrong number of arguments for \'ZINCRBY\' command' };
8
+ }
9
+ try {
10
+ const score = engine.zincrby(args[0], args[1], args[2]);
11
+ return score;
12
+ } catch (e) {
13
+ const msg = e && e.message ? e.message : String(e);
14
+ return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
15
+ }
16
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * ZREMRANGEBYRANK key start stop - removes members in rank range. Returns count removed.
3
+ */
4
+
5
+ export function handleZremrangebyrank(engine, args) {
6
+ if (!args || args.length < 3) {
7
+ return { error: 'ERR wrong number of arguments for \'ZREMRANGEBYRANK\' command' };
8
+ }
9
+ try {
10
+ const start = parseInt(String(args[1]), 10);
11
+ const stop = parseInt(String(args[2]), 10);
12
+ if (Number.isNaN(start) || Number.isNaN(stop)) {
13
+ return { error: 'ERR value is not an integer or out of range' };
14
+ }
15
+ const n = engine.zremrangebyrank(args[0], start, stop);
16
+ return n;
17
+ } catch (e) {
18
+ const msg = e && e.message ? e.message : String(e);
19
+ return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * ZREMRANGEBYSCORE key min max - removes members with score in [min, max]. Returns count removed.
3
+ */
4
+
5
+ export function handleZremrangebyscore(engine, args) {
6
+ if (!args || args.length < 3) {
7
+ return { error: 'ERR wrong number of arguments for \'ZREMRANGEBYSCORE\' command' };
8
+ }
9
+ try {
10
+ const min = parseFloat(String(args[1]));
11
+ const max = parseFloat(String(args[2]));
12
+ if (Number.isNaN(min) || Number.isNaN(max)) {
13
+ return { error: 'ERR value is not a valid float' };
14
+ }
15
+ const n = engine.zremrangebyscore(args[0], min, max);
16
+ return n;
17
+ } catch (e) {
18
+ const msg = e && e.message ? e.message : String(e);
19
+ return { error: msg.startsWith('ERR ') ? msg : msg.startsWith('WRONGTYPE') ? msg : 'ERR ' + msg };
20
+ }
21
+ }
@@ -10,6 +10,7 @@ import { createSetsStorage } from '../storage/sqlite/sets.js';
10
10
  import { createListsStorage } from '../storage/sqlite/lists.js';
11
11
  import { createZsetsStorage } from '../storage/sqlite/zsets.js';
12
12
  import { createBlockingManager } from '../blocking/manager.js';
13
+ import { runInTransaction } from '../storage/sqlite/tx.js';
13
14
  import { expectString, expectHash, expectSet, expectList, expectZset, typeName } from './validate.js';
14
15
  import { asKey, asValue } from '../util/buffers.js';
15
16
 
@@ -55,6 +56,15 @@ export function createEngine(opts = {}) {
55
56
  return strings.get(k);
56
57
  },
57
58
 
59
+ strlen(key) {
60
+ const k = asKey(key);
61
+ const meta = getKeyMeta(key);
62
+ if (!meta) return 0;
63
+ expectString(meta);
64
+ const v = strings.get(k);
65
+ return v ? v.length : 0;
66
+ },
67
+
58
68
  set(key, value, options = {}) {
59
69
  const k = asKey(key);
60
70
  getKeyMeta(key); // lazy-expire if needed
@@ -201,6 +211,20 @@ export function createEngine(opts = {}) {
201
211
  return hashes.getAll(k);
202
212
  },
203
213
 
214
+ hkeys(key) {
215
+ const flat = this.hgetall(key);
216
+ const out = [];
217
+ for (let i = 0; i < flat.length; i += 2) out.push(flat[i]);
218
+ return out;
219
+ },
220
+
221
+ hvals(key) {
222
+ const flat = this.hgetall(key);
223
+ const out = [];
224
+ for (let i = 1; i < flat.length; i += 2) out.push(flat[i]);
225
+ return out;
226
+ },
227
+
204
228
  hdel(key, fields) {
205
229
  const k = asKey(key);
206
230
  const meta = getKeyMeta(key);
@@ -267,6 +291,22 @@ export function createEngine(opts = {}) {
267
291
  return sets.count(asKey(key));
268
292
  },
269
293
 
294
+ spop(key, count = null) {
295
+ const k = asKey(key);
296
+ const meta = getKeyMeta(key);
297
+ if (!meta) return count != null && count !== 1 ? [] : null;
298
+ expectSet(meta);
299
+ return sets.popRandom(k, count);
300
+ },
301
+
302
+ srandmember(key, count = null) {
303
+ const k = asKey(key);
304
+ const meta = getKeyMeta(key);
305
+ if (!meta) return count != null && count !== 1 ? [] : null;
306
+ expectSet(meta);
307
+ return sets.getRandomMembers(k, count);
308
+ },
309
+
270
310
  lpush(key, ...values) {
271
311
  const k = asKey(key);
272
312
  getKeyMeta(key);
@@ -341,6 +381,28 @@ export function createEngine(opts = {}) {
341
381
  return lists.lrem(k, c, elem);
342
382
  },
343
383
 
384
+ lset(key, index, value) {
385
+ const k = asKey(key);
386
+ const meta = getKeyMeta(key);
387
+ if (!meta) throw new Error('ERR no such key');
388
+ expectList(meta);
389
+ const i = parseInt(Buffer.isBuffer(index) ? index.toString() : String(index), 10);
390
+ if (Number.isNaN(i)) throw new Error('ERR index out of range');
391
+ const val = Buffer.isBuffer(value) ? value : asValue(value);
392
+ lists.lset(k, i, val);
393
+ },
394
+
395
+ ltrim(key, start, stop) {
396
+ const k = asKey(key);
397
+ const meta = getKeyMeta(key);
398
+ if (!meta) return;
399
+ expectList(meta);
400
+ const s = parseInt(Buffer.isBuffer(start) ? start.toString() : String(start), 10);
401
+ const e = parseInt(Buffer.isBuffer(stop) ? stop.toString() : String(stop), 10);
402
+ if (Number.isNaN(s) || Number.isNaN(e)) return;
403
+ lists.ltrim(k, s, e);
404
+ },
405
+
344
406
  zadd(key, scoreMemberPairs) {
345
407
  const k = asKey(key);
346
408
  getKeyMeta(key);
@@ -423,6 +485,44 @@ export function createEngine(opts = {}) {
423
485
  });
424
486
  },
425
487
 
488
+ zcount(key, min, max) {
489
+ const k = asKey(key);
490
+ const meta = getKeyMeta(key);
491
+ if (!meta) return 0;
492
+ expectZset(meta);
493
+ return zsets.countByScore(k, min, max);
494
+ },
495
+
496
+ zincrby(key, increment, member) {
497
+ const k = asKey(key);
498
+ getKeyMeta(key);
499
+ const inc = parseFloat(Buffer.isBuffer(increment) ? increment.toString() : String(increment));
500
+ if (Number.isNaN(inc)) throw new Error('ERR value is not a valid float');
501
+ return zsets.incr(k, asKey(member), inc);
502
+ },
503
+
504
+ zremrangebyrank(key, start, stop) {
505
+ const k = asKey(key);
506
+ const meta = getKeyMeta(key);
507
+ if (!meta) return 0;
508
+ expectZset(meta);
509
+ const s = parseInt(Buffer.isBuffer(start) ? start.toString() : String(start), 10);
510
+ const e = parseInt(Buffer.isBuffer(stop) ? stop.toString() : String(stop), 10);
511
+ if (Number.isNaN(s) || Number.isNaN(e)) return 0;
512
+ return zsets.removeRangeByRank(k, s, e);
513
+ },
514
+
515
+ zremrangebyscore(key, min, max) {
516
+ const k = asKey(key);
517
+ const meta = getKeyMeta(key);
518
+ if (!meta) return 0;
519
+ expectZset(meta);
520
+ const minNum = parseFloat(Buffer.isBuffer(min) ? min.toString() : String(min));
521
+ const maxNum = parseFloat(Buffer.isBuffer(max) ? max.toString() : String(max));
522
+ if (Number.isNaN(minNum) || Number.isNaN(maxNum)) throw new Error('ERR value is not a valid float');
523
+ return zsets.removeRangeByScore(k, minNum, maxNum);
524
+ },
525
+
426
526
  zrevrangebyscore(key, max, min, options = {}) {
427
527
  const k = asKey(key);
428
528
  const meta = getKeyMeta(key);
@@ -459,6 +559,38 @@ export function createEngine(opts = {}) {
459
559
  return { cursor: nextCursor, keys: keysList };
460
560
  },
461
561
 
562
+ rename(key, newkey) {
563
+ const k = asKey(key);
564
+ const nk = asKey(newkey);
565
+ const meta = getKeyMeta(key);
566
+ if (!meta) throw new Error('ERR no such key');
567
+ if (k.equals(nk)) return;
568
+ runInTransaction(db, () => {
569
+ if (keys.get(nk)) keys.delete(nk);
570
+ keys.set(nk, meta.type, { expiresAt: meta.expiresAt });
571
+ switch (meta.type) {
572
+ case KEY_TYPES.STRING:
573
+ strings.copyKey(k, nk);
574
+ break;
575
+ case KEY_TYPES.HASH:
576
+ hashes.copyKey(k, nk);
577
+ break;
578
+ case KEY_TYPES.SET:
579
+ sets.copyKey(k, nk);
580
+ break;
581
+ case KEY_TYPES.LIST:
582
+ lists.copyKey(k, nk);
583
+ break;
584
+ case KEY_TYPES.ZSET:
585
+ zsets.copyKey(k, nk);
586
+ break;
587
+ default:
588
+ throw new Error('ERR unknown key type');
589
+ }
590
+ keys.delete(k);
591
+ });
592
+ },
593
+
462
594
  // Expose for storage/commands that need direct access
463
595
  _db: db,
464
596
  _cache: cache ?? null,
@@ -103,5 +103,13 @@ export function createHashesStorage(db, keys) {
103
103
  return next;
104
104
  });
105
105
  },
106
+
107
+ /** Copy all field/value rows from oldKey to newKey. Caller ensures newKey exists in redis_keys. */
108
+ copyKey(oldKey, newKey) {
109
+ const rows = getAllStmt.all(oldKey);
110
+ for (let i = 0; i < rows.length; i += 2) {
111
+ insertStmt.run(newKey, rows[i], rows[i + 1]);
112
+ }
113
+ },
106
114
  };
107
115
  }
@@ -41,6 +41,15 @@ export function createListsStorage(db, keys) {
41
41
  'SELECT seq, value FROM redis_list_items WHERE key = ? ORDER BY seq ASC'
42
42
  );
43
43
  const deleteAllItemsStmt = db.prepare('DELETE FROM redis_list_items WHERE key = ?');
44
+ const updateItemStmt = db.prepare(
45
+ 'UPDATE redis_list_items SET value = ? WHERE key = ? AND seq = ?'
46
+ );
47
+ const deleteBeforeSeqStmt = db.prepare(
48
+ 'DELETE FROM redis_list_items WHERE key = ? AND seq < ?'
49
+ );
50
+ const deleteAfterSeqStmt = db.prepare(
51
+ 'DELETE FROM redis_list_items WHERE key = ? AND seq > ?'
52
+ );
44
53
 
45
54
  function getMeta(key) {
46
55
  return getMetaStmt.get(key) || null;
@@ -279,5 +288,56 @@ export function createListsStorage(db, keys) {
279
288
  return toDelete.length;
280
289
  });
281
290
  },
291
+
292
+ lset(key, index, value) {
293
+ return runInTransaction(db, () => {
294
+ const meta = getMeta(key);
295
+ const len = length(meta);
296
+ if (len === 0) throw new Error('ERR no such key');
297
+ const idx = parseInt(String(index), 10);
298
+ if (Number.isNaN(idx)) throw new Error('ERR index out of range');
299
+ const i = idx < 0 ? len + idx : idx;
300
+ if (i < 0 || i >= len) throw new Error('ERR index out of range');
301
+ const seq = meta.headSeq + i;
302
+ const r = updateItemStmt.run(value, key, seq);
303
+ if (r.changes === 0) throw new Error('ERR index out of range');
304
+ keys.bumpVersion(key);
305
+ });
306
+ },
307
+
308
+ ltrim(key, start, stop) {
309
+ return runInTransaction(db, () => {
310
+ const meta = getMeta(key);
311
+ const len = length(meta);
312
+ if (len === 0) return;
313
+
314
+ let s = start < 0 ? Math.max(0, len + start) : Math.min(start, len - 1);
315
+ let e = stop < 0 ? Math.max(0, len + stop) : Math.min(stop, len - 1);
316
+ if (s > e) {
317
+ deleteAllItemsStmt.run(key);
318
+ deleteMetaStmt.run(key);
319
+ keys.delete(key);
320
+ return;
321
+ }
322
+
323
+ const newHead = meta.headSeq + s;
324
+ const newTail = meta.headSeq + e;
325
+ deleteBeforeSeqStmt.run(key, newHead);
326
+ deleteAfterSeqStmt.run(key, newTail);
327
+ updateMetaStmt.run(newHead, newTail, key);
328
+ keys.bumpVersion(key);
329
+ });
330
+ },
331
+
332
+ /** Copy list from oldKey to newKey. Caller ensures newKey exists in redis_keys. */
333
+ copyKey(oldKey, newKey) {
334
+ const meta = getMeta(oldKey);
335
+ if (!meta) return;
336
+ insertMetaStmt.run(newKey, meta.headSeq, meta.tailSeq);
337
+ const rows = selectAllWithSeqStmt.all(oldKey);
338
+ for (const r of rows) {
339
+ insertItemStmt.run(newKey, r.seq, r.value);
340
+ }
341
+ },
282
342
  };
283
343
  }
@@ -68,5 +68,65 @@ export function createSetsStorage(db, keys) {
68
68
  const row = countStmt.get(key);
69
69
  return row ? row.n : 0;
70
70
  },
71
+
72
+ /** Copy all members from oldKey to newKey. Caller ensures newKey exists in redis_keys. */
73
+ copyKey(oldKey, newKey) {
74
+ const rows = membersStmt.all(oldKey);
75
+ for (const r of rows) {
76
+ insertStmt.run(newKey, r.member);
77
+ }
78
+ },
79
+
80
+ /** Get random members without removing. count null/1 = single; count > 0 = up to count distinct; count < 0 = |count| with replacement. */
81
+ getRandomMembers(key, count) {
82
+ const arr = membersStmt.all(key).map((r) => r.member);
83
+ if (arr.length === 0) return count != null && count !== 1 ? [] : null;
84
+ const c = count == null ? 1 : count;
85
+ if (c === 1) return arr[Math.floor(Math.random() * arr.length)];
86
+ if (c > 0) {
87
+ const shuffled = arr.slice();
88
+ for (let i = shuffled.length - 1; i > 0; i--) {
89
+ const j = Math.floor(Math.random() * (i + 1));
90
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
91
+ }
92
+ return shuffled.slice(0, Math.min(c, shuffled.length));
93
+ }
94
+ const out = [];
95
+ for (let i = 0; i < -c; i++) out.push(arr[Math.floor(Math.random() * arr.length)]);
96
+ return out;
97
+ },
98
+
99
+ /** Remove and return random members. Returns single member or array. */
100
+ popRandom(key, count, options = {}) {
101
+ return runInTransaction(db, () => {
102
+ const arr = membersStmt.all(key).map((r) => r.member);
103
+ if (arr.length === 0) return count != null && count !== 1 ? [] : null;
104
+ const c = count == null ? 1 : count;
105
+ let chosen;
106
+ if (c === 1) {
107
+ chosen = [arr[Math.floor(Math.random() * arr.length)]];
108
+ } else if (c > 0) {
109
+ const shuffled = arr.slice();
110
+ for (let i = shuffled.length - 1; i > 0; i--) {
111
+ const j = Math.floor(Math.random() * (i + 1));
112
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
113
+ }
114
+ chosen = shuffled.slice(0, Math.min(c, shuffled.length));
115
+ } else {
116
+ chosen = [];
117
+ for (let i = 0; i < -c; i++) chosen.push(arr[Math.floor(Math.random() * arr.length)]);
118
+ }
119
+ for (const m of chosen) deleteStmt.run(key, m);
120
+ const row = countStmt.get(key);
121
+ const remaining = (row && row.n) || 0;
122
+ if (remaining === 0) {
123
+ deleteAllStmt.run(key);
124
+ keys.delete(key);
125
+ } else {
126
+ keys.bumpVersion(key);
127
+ }
128
+ return c === 1 ? chosen[0] : chosen;
129
+ });
130
+ },
71
131
  };
72
132
  }
@@ -59,5 +59,13 @@ export function createStringsStorage(db, keys) {
59
59
  keys.delete(key);
60
60
  });
61
61
  },
62
+
63
+ /**
64
+ * Copy key's value to newKey. Caller must ensure newKey row exists in redis_keys.
65
+ */
66
+ copyKey(oldKey, newKey) {
67
+ const row = getStmt.get(oldKey);
68
+ if (row) insertStmt.run(newKey, row.value);
69
+ },
62
70
  };
63
71
  }
@@ -62,6 +62,13 @@ export function createZsetsStorage(db, keys) {
62
62
  ORDER BY score DESC, member DESC
63
63
  LIMIT ? OFFSET ?`
64
64
  );
65
+ const selectAllStmt = db.prepare('SELECT member, score FROM redis_zsets WHERE key = ?');
66
+ const countByScoreStmt = db.prepare(
67
+ 'SELECT COUNT(*) AS n FROM redis_zsets WHERE key = ? AND score >= ? AND score <= ?'
68
+ );
69
+ const deleteByScoreRangeStmt = db.prepare(
70
+ 'DELETE FROM redis_zsets WHERE key = ? AND score >= ? AND score <= ?'
71
+ );
65
72
 
66
73
  return {
67
74
  /**
@@ -256,5 +263,79 @@ export function createZsetsStorage(db, keys) {
256
263
  }
257
264
  return out;
258
265
  },
266
+
267
+ /** Copy all member/score rows from oldKey to newKey. Caller ensures newKey exists in redis_keys. */
268
+ copyKey(oldKey, newKey) {
269
+ const rows = selectAllStmt.all(oldKey);
270
+ for (const r of rows) {
271
+ upsertStmt.run(newKey, r.member, r.score);
272
+ }
273
+ },
274
+
275
+ countByScore(key, min, max) {
276
+ const row = countByScoreStmt.get(key, min, max);
277
+ return row ? row.n : 0;
278
+ },
279
+
280
+ incr(key, member, increment, options = {}) {
281
+ return runInTransaction(db, () => {
282
+ const now = options.updatedAt ?? Date.now();
283
+ const meta = keys.get(key);
284
+ if (meta && meta.type !== KEY_TYPES.ZSET) {
285
+ throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
286
+ }
287
+ if (!meta) {
288
+ keys.set(key, KEY_TYPES.ZSET, { updatedAt: now });
289
+ } else {
290
+ keys.bumpVersion(key);
291
+ }
292
+ const cur = scoreStmt.get(key, member);
293
+ const prev = cur == null ? 0 : cur.score;
294
+ const next = prev + increment;
295
+ upsertStmt.run(key, member, next);
296
+ return formatScore(next);
297
+ });
298
+ },
299
+
300
+ removeRangeByRank(key, start, stop) {
301
+ return runInTransaction(db, () => {
302
+ const len = this.count(key);
303
+ if (len === 0) return 0;
304
+ let s = start >= 0 ? start : Math.max(0, len + start);
305
+ let e = stop >= 0 ? stop : Math.max(0, len + stop);
306
+ if (s > e) return 0;
307
+ s = Math.min(s, len - 1);
308
+ e = Math.min(e, len - 1);
309
+ const limit = e - s + 1;
310
+ const offset = s;
311
+ const rows = rangeByRankStmt.all(key, limit, offset);
312
+ let n = 0;
313
+ for (const r of rows) {
314
+ n += deleteStmt.run(key, r.member).changes;
315
+ }
316
+ const row = countStmt.get(key);
317
+ const remaining = (row && row.n) || 0;
318
+ if (remaining === 0) {
319
+ deleteAllStmt.run(key);
320
+ keys.delete(key);
321
+ }
322
+ return n;
323
+ });
324
+ },
325
+
326
+ removeRangeByScore(key, min, max) {
327
+ return runInTransaction(db, () => {
328
+ const r = deleteByScoreRangeStmt.run(key, min, max);
329
+ const remaining = countStmt.get(key);
330
+ const n = (remaining && remaining.n) || 0;
331
+ if (n === 0) {
332
+ deleteAllStmt.run(key);
333
+ keys.delete(key);
334
+ } else if (r.changes > 0) {
335
+ keys.bumpVersion(key);
336
+ }
337
+ return r.changes;
338
+ });
339
+ },
259
340
  };
260
341
  }
@@ -0,0 +1,135 @@
1
+ import { describe, it, before, after } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createTestServer } from '../helpers/server.js';
4
+ import { sendCommand, argv } from '../helpers/client.js';
5
+ import { tryParseValue } from '../../src/resp/parser.js';
6
+
7
+ function parseIntegerReply(buf) {
8
+ const r = tryParseValue(buf, 0);
9
+ return r && typeof r.value === 'number' ? r.value : null;
10
+ }
11
+
12
+ function parseBulkReply(buf) {
13
+ const r = tryParseValue(buf, 0);
14
+ return r && r.value && Buffer.isBuffer(r.value) ? r.value.toString('utf8') : null;
15
+ }
16
+
17
+ function parseArrayReply(buf) {
18
+ const r = tryParseValue(buf, 0);
19
+ if (!r || !Array.isArray(r.value)) return null;
20
+ return r.value.map((v) => (Buffer.isBuffer(v) ? v.toString('utf8') : v));
21
+ }
22
+
23
+ describe('New commands integration (STRLEN, HKEYS, HVALS, LSET, LTRIM, RENAME, Z*, SPOP, SRANDMEMBER)', () => {
24
+ let s;
25
+ let port;
26
+
27
+ before(async () => {
28
+ s = await createTestServer();
29
+ port = s.port;
30
+ });
31
+
32
+ after(async () => {
33
+ await s.closeAsync();
34
+ });
35
+
36
+ it('STRLEN returns byte length, 0 for missing key', async () => {
37
+ await sendCommand(port, argv('SET', 'slen', 'hello'));
38
+ const r = await sendCommand(port, argv('STRLEN', 'slen'));
39
+ assert.equal(parseIntegerReply(r), 5);
40
+ const r0 = await sendCommand(port, argv('STRLEN', 'nonexistent'));
41
+ assert.equal(parseIntegerReply(r0), 0);
42
+ });
43
+
44
+ it('HKEYS returns field names', async () => {
45
+ await sendCommand(port, argv('HSET', 'hk', 'a', '1', 'b', '2'));
46
+ const r = await sendCommand(port, argv('HKEYS', 'hk'));
47
+ const arr = parseArrayReply(r);
48
+ assert.ok(Array.isArray(arr));
49
+ assert.ok(arr.includes('a') && arr.includes('b'));
50
+ const empty = await sendCommand(port, argv('HKEYS', 'nonexistent'));
51
+ assert.equal(parseArrayReply(empty).length, 0);
52
+ });
53
+
54
+ it('HVALS returns values', async () => {
55
+ await sendCommand(port, argv('HSET', 'hv', 'x', '10', 'y', '20'));
56
+ const r = await sendCommand(port, argv('HVALS', 'hv'));
57
+ const arr = parseArrayReply(r);
58
+ assert.ok(arr.includes('10') && arr.includes('20'));
59
+ });
60
+
61
+ it('LSET sets element at index and returns OK', async () => {
62
+ await sendCommand(port, argv('RPUSH', 'lst', 'one', 'two', 'three'));
63
+ const ok = await sendCommand(port, argv('LSET', 'lst', '1', 'TWO'));
64
+ assert.ok(ok.toString('utf8').startsWith('+OK'));
65
+ const r = await sendCommand(port, argv('LRANGE', 'lst', '0', '-1'));
66
+ const arr = parseArrayReply(r);
67
+ assert.equal(arr[1], 'TWO');
68
+ });
69
+
70
+ it('LTRIM keeps only range and returns OK', async () => {
71
+ await sendCommand(port, argv('RPUSH', 'lt', 'a', 'b', 'c', 'd'));
72
+ await sendCommand(port, argv('LTRIM', 'lt', '1', '2'));
73
+ const r = await sendCommand(port, argv('LRANGE', 'lt', '0', '-1'));
74
+ const arr = parseArrayReply(r);
75
+ assert.deepEqual(arr, ['b', 'c']);
76
+ });
77
+
78
+ it('RENAME renames key and overwrites destination', async () => {
79
+ await sendCommand(port, argv('SET', 'old', 'value'));
80
+ const ok = await sendCommand(port, argv('RENAME', 'old', 'new'));
81
+ assert.ok(ok.toString('utf8').startsWith('+OK'));
82
+ const v = await sendCommand(port, argv('GET', 'new'));
83
+ assert.equal(parseBulkReply(v), 'value');
84
+ const missing = await sendCommand(port, argv('GET', 'old'));
85
+ assert.equal(missing.toString('ascii'), '$-1\r\n');
86
+ });
87
+
88
+ it('ZCOUNT returns count in score range', async () => {
89
+ await sendCommand(port, argv('ZADD', 'zc', '1', 'a', '2', 'b', '3', 'c'));
90
+ const r = await sendCommand(port, argv('ZCOUNT', 'zc', '1', '2'));
91
+ assert.equal(parseIntegerReply(r), 2);
92
+ assert.equal(parseIntegerReply(await sendCommand(port, argv('ZCOUNT', 'zc', '10', '20'))), 0);
93
+ });
94
+
95
+ it('ZINCRBY increments score and returns new score', async () => {
96
+ await sendCommand(port, argv('ZADD', 'zi', '10', 'm'));
97
+ const r = await sendCommand(port, argv('ZINCRBY', 'zi', '5', 'm'));
98
+ const score = r.toString('utf8').replace(/\r\n$/, '');
99
+ assert.ok(score.includes('15'));
100
+ const r2 = await sendCommand(port, argv('ZINCRBY', 'zi', '1', 'new'));
101
+ assert.ok(r2.toString('utf8').includes('1'));
102
+ });
103
+
104
+ it('ZREMRANGEBYRANK removes by rank and returns count', async () => {
105
+ await sendCommand(port, argv('ZADD', 'zr', '1', 'a', '2', 'b', '3', 'c'));
106
+ const n = await sendCommand(port, argv('ZREMRANGEBYRANK', 'zr', '0', '0'));
107
+ assert.equal(parseIntegerReply(n), 1);
108
+ const arr = await sendCommand(port, argv('ZRANGE', 'zr', '0', '-1'));
109
+ assert.equal(parseArrayReply(arr).length, 2);
110
+ });
111
+
112
+ it('ZREMRANGEBYSCORE removes by score range and returns count', async () => {
113
+ await sendCommand(port, argv('ZADD', 'zs', '1', 'a', '2', 'b', '3', 'c'));
114
+ const n = await sendCommand(port, argv('ZREMRANGEBYSCORE', 'zs', '1', '2'));
115
+ assert.equal(parseIntegerReply(n), 2);
116
+ const arr = await sendCommand(port, argv('ZRANGE', 'zs', '0', '-1'));
117
+ assert.deepEqual(parseArrayReply(arr), ['c']);
118
+ });
119
+
120
+ it('SPOP removes and returns random member(s)', async () => {
121
+ await sendCommand(port, argv('SADD', 'sp', 'x', 'y', 'z'));
122
+ const one = await sendCommand(port, argv('SPOP', 'sp'));
123
+ const v = parseBulkReply(one);
124
+ assert.ok(['x', 'y', 'z'].includes(v));
125
+ const card = await sendCommand(port, argv('SCARD', 'sp'));
126
+ assert.equal(parseIntegerReply(card), 2);
127
+ });
128
+
129
+ it('SRANDMEMBER returns random member without removing', async () => {
130
+ await sendCommand(port, argv('SADD', 'sr', 'a', 'b'));
131
+ const r = await sendCommand(port, argv('SRANDMEMBER', 'sr'));
132
+ assert.ok(['a', 'b'].includes(parseBulkReply(r)));
133
+ assert.equal(parseIntegerReply(await sendCommand(port, argv('SCARD', 'sr'))), 2);
134
+ });
135
+ });