resplite 1.2.16 → 1.2.18
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/package.json
CHANGED
package/src/commands/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLIENT - connection introspection (SETNAME, GETNAME, ID).
|
|
2
|
+
* CLIENT - connection introspection (SETNAME, GETNAME, ID) and SETINFO (no-op for client lib version).
|
|
3
3
|
* Requires connection context.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -31,5 +31,11 @@ export function handleClient(engine, args, context) {
|
|
|
31
31
|
}
|
|
32
32
|
return context.connectionId;
|
|
33
33
|
}
|
|
34
|
+
if (sub === 'SETINFO') {
|
|
35
|
+
if (args.length !== 3) {
|
|
36
|
+
return { error: 'ERR wrong number of arguments for \'CLIENT SETINFO\' command' };
|
|
37
|
+
}
|
|
38
|
+
return { simple: 'OK' };
|
|
39
|
+
}
|
|
34
40
|
return { error: 'ERR unknown subcommand or wrong number of arguments for \'CLIENT\'. Try CLIENT HELP.' };
|
|
35
41
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COMMAND - introspection: list commands, count, or info for specific commands.
|
|
3
|
+
* Reply format compatible with Redis COMMAND (array of [name, arity, flags, firstKey, lastKey, step, acl_categories]).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @type {Set<string>} Commands that modify data (write). */
|
|
7
|
+
const WRITE_COMMANDS = new Set([
|
|
8
|
+
'SET', 'MSET', 'DEL', 'UNLINK', 'EXPIRE', 'PEXPIRE', 'PERSIST', 'INCR', 'DECR', 'INCRBY', 'DECRBY',
|
|
9
|
+
'HSET', 'HDEL', 'HINCRBY', 'SADD', 'SREM', 'LPUSH', 'RPUSH', 'LPOP', 'RPOP', 'LREM', 'ZADD', 'ZREM',
|
|
10
|
+
'FT.CREATE', 'FT.ADD', 'FT.DEL', 'FT.SUGADD', 'FT.SUGDEL', 'CLIENT',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build Redis-style command doc: [name, arity, flags, firstKey, lastKey, step, acl_categories].
|
|
15
|
+
* @param {string} name - Command name (lowercase for reply).
|
|
16
|
+
* @returns {Array<string|number|string[]>}
|
|
17
|
+
*/
|
|
18
|
+
function docFor(name) {
|
|
19
|
+
const lower = name.toLowerCase();
|
|
20
|
+
const flags = WRITE_COMMANDS.has(name) ? ['write', 'fast'] : ['readonly', 'fast'];
|
|
21
|
+
let arity = 2;
|
|
22
|
+
let firstKey = 1;
|
|
23
|
+
let lastKey = 1;
|
|
24
|
+
let step = 1;
|
|
25
|
+
if (['MGET', 'MSET', 'DEL', 'UNLINK', 'EXISTS', 'KEYS', 'SCAN', 'PING', 'ECHO', 'QUIT', 'TYPE', 'OBJECT', 'SQLITE.INFO', 'CACHE.INFO', 'MEMORY.INFO', 'COMMAND', 'MONITOR', 'CLIENT'].includes(name)) {
|
|
26
|
+
if (['PING', 'ECHO', 'QUIT', 'COMMAND', 'MONITOR'].includes(name)) {
|
|
27
|
+
firstKey = 0;
|
|
28
|
+
lastKey = 0;
|
|
29
|
+
step = 0;
|
|
30
|
+
arity = name === 'COMMAND' ? -1 : (name === 'ECHO' ? 2 : 1);
|
|
31
|
+
} else if (['MGET', 'EXISTS', 'KEYS', 'SCAN'].includes(name)) {
|
|
32
|
+
arity = -2;
|
|
33
|
+
lastKey = -1;
|
|
34
|
+
} else if (name === 'MSET') {
|
|
35
|
+
arity = -3;
|
|
36
|
+
lastKey = -1;
|
|
37
|
+
step = 2;
|
|
38
|
+
} else if (['DEL', 'UNLINK'].includes(name)) {
|
|
39
|
+
arity = -2;
|
|
40
|
+
lastKey = -1;
|
|
41
|
+
}
|
|
42
|
+
} else if (name.startsWith('FT.') || name.startsWith('SQLITE.') || name.startsWith('CACHE.') || name.startsWith('MEMORY.')) {
|
|
43
|
+
firstKey = 0;
|
|
44
|
+
lastKey = 0;
|
|
45
|
+
step = 0;
|
|
46
|
+
arity = -2;
|
|
47
|
+
} else if (['BLPOP', 'BRPOP'].includes(name)) {
|
|
48
|
+
arity = -3;
|
|
49
|
+
lastKey = -1;
|
|
50
|
+
step = 1;
|
|
51
|
+
} else if (['HMGET', 'HGETALL', 'HGET', 'HSET', 'HDEL', 'HEXISTS', 'HINCRBY', 'HLEN'].includes(name)) {
|
|
52
|
+
arity = (name === 'HGET' || name === 'HLEN' || name === 'HEXISTS') ? 3 : -3;
|
|
53
|
+
}
|
|
54
|
+
return [lower, arity, flags, firstKey, lastKey, step, []];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {object} engine
|
|
59
|
+
* @param {Buffer[]} args - subcommand and optional names for INFO
|
|
60
|
+
* @param {{ getCommandNames?: () => string[] }} context
|
|
61
|
+
*/
|
|
62
|
+
export function handleCommand(engine, args, context) {
|
|
63
|
+
const allNames = context?.getCommandNames ? context.getCommandNames() : [];
|
|
64
|
+
const sub = (args && args.length > 0 && Buffer.isBuffer(args[0])) ? args[0].toString('utf8').toUpperCase() : '';
|
|
65
|
+
|
|
66
|
+
if (!sub || sub === '') {
|
|
67
|
+
const reply = allNames.map((n) => docFor(n));
|
|
68
|
+
return reply;
|
|
69
|
+
}
|
|
70
|
+
if (sub === 'COUNT') {
|
|
71
|
+
return allNames.length;
|
|
72
|
+
}
|
|
73
|
+
if (sub === 'INFO') {
|
|
74
|
+
const names = (args.slice(1) || []).map((b) => (Buffer.isBuffer(b) ? b.toString('utf8') : String(b)).toUpperCase());
|
|
75
|
+
const set = new Set(allNames);
|
|
76
|
+
const reply = names.map((n) => set.has(n) ? docFor(n) : null).filter((x) => x != null);
|
|
77
|
+
return reply;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { error: 'ERR unknown subcommand or wrong number of arguments for \'COMMAND\'. Try COMMAND HELP.' };
|
|
81
|
+
}
|
package/src/commands/registry.js
CHANGED
|
@@ -72,6 +72,7 @@ import * as ftSugget from './ft-sugget.js';
|
|
|
72
72
|
import * as ftSugdel from './ft-sugdel.js';
|
|
73
73
|
import * as monitor from './monitor.js';
|
|
74
74
|
import * as client from './client.js';
|
|
75
|
+
import * as command from './command.js';
|
|
75
76
|
|
|
76
77
|
const HANDLERS = new Map([
|
|
77
78
|
['PING', (e, a) => ping.handlePing()],
|
|
@@ -143,6 +144,7 @@ const HANDLERS = new Map([
|
|
|
143
144
|
['FT.SUGDEL', (e, a) => ftSugdel.handleFtSugdel(e, a)],
|
|
144
145
|
['MONITOR', (e, a, ctx) => monitor.handleMonitor(a, ctx)],
|
|
145
146
|
['CLIENT', (e, a, ctx) => client.handleClient(e, a, ctx)],
|
|
147
|
+
['COMMAND', (e, a, ctx) => command.handleCommand(e, a, ctx)],
|
|
146
148
|
]);
|
|
147
149
|
|
|
148
150
|
/**
|
|
@@ -159,6 +161,7 @@ export function dispatch(engine, argv, context) {
|
|
|
159
161
|
const cmd = (Buffer.isBuffer(argv[0]) ? argv[0].toString('utf8') : String(argv[0])).toUpperCase();
|
|
160
162
|
const args = argv.slice(1);
|
|
161
163
|
const argvStrings = argv.map((b) => (Buffer.isBuffer(b) ? b.toString('utf8') : String(b)));
|
|
164
|
+
if (context) context.getCommandNames = () => Array.from(HANDLERS.keys());
|
|
162
165
|
const handler = HANDLERS.get(cmd);
|
|
163
166
|
if (!handler) {
|
|
164
167
|
context?.onUnknownCommand?.({
|
|
@@ -87,6 +87,13 @@ describe('CLIENT integration', () => {
|
|
|
87
87
|
assert.equal(getResult.length, 0);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
it('CLIENT SETINFO LIB-VER / LIB-NAME returns OK (no-op)', async () => {
|
|
91
|
+
const reply = await sendCommand(port, argv('CLIENT', 'SETINFO', 'LIB-VER', '1.5.17'));
|
|
92
|
+
assert.equal(tryParseValue(reply, 0).value, 'OK');
|
|
93
|
+
const reply2 = await sendCommand(port, argv('CLIENT', 'SETINFO', 'LIB-NAME', 'myclient'));
|
|
94
|
+
assert.equal(tryParseValue(reply2, 0).value, 'OK');
|
|
95
|
+
});
|
|
96
|
+
|
|
90
97
|
it('CLIENT wrong subcommand returns error', async () => {
|
|
91
98
|
const reply = await sendCommand(port, argv('CLIENT', 'LIST'));
|
|
92
99
|
const v = tryParseValue(reply, 0).value;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for COMMAND (introspection).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, before, after } from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { createTestServer } from '../helpers/server.js';
|
|
8
|
+
import { sendCommand, argv } from '../helpers/client.js';
|
|
9
|
+
import { tryParseValue } from '../../src/resp/parser.js';
|
|
10
|
+
|
|
11
|
+
describe('COMMAND integration', () => {
|
|
12
|
+
let s;
|
|
13
|
+
let port;
|
|
14
|
+
|
|
15
|
+
before(async () => {
|
|
16
|
+
s = await createTestServer();
|
|
17
|
+
port = s.port;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
after(async () => {
|
|
21
|
+
await s.closeAsync();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('COMMAND (no args) returns array of command docs', async () => {
|
|
25
|
+
const reply = await sendCommand(port, argv('COMMAND'));
|
|
26
|
+
const v = tryParseValue(reply, 0).value;
|
|
27
|
+
assert.ok(Array.isArray(v), 'expected array');
|
|
28
|
+
assert.ok(v.length > 0, 'expected at least one command');
|
|
29
|
+
const pingDoc = v.find((doc) => {
|
|
30
|
+
const n = Buffer.isBuffer(doc[0]) ? doc[0].toString('utf8') : doc[0];
|
|
31
|
+
return n === 'ping';
|
|
32
|
+
});
|
|
33
|
+
assert.ok(pingDoc, 'expected ping in command list');
|
|
34
|
+
const first = pingDoc;
|
|
35
|
+
assert.ok(Array.isArray(first), 'each doc is array');
|
|
36
|
+
const name = Buffer.isBuffer(first[0]) ? first[0].toString('utf8') : first[0];
|
|
37
|
+
assert.strictEqual(name, 'ping', 'first element is name (lowercase)');
|
|
38
|
+
assert.strictEqual(typeof first[1], 'number', 'arity');
|
|
39
|
+
assert.ok(Array.isArray(first[2]), 'flags');
|
|
40
|
+
const flags = first[2].map((f) => (Buffer.isBuffer(f) ? f.toString('utf8') : f));
|
|
41
|
+
assert.ok(flags.includes('readonly') || flags.includes('write'));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('COMMAND COUNT returns integer', async () => {
|
|
45
|
+
const reply = await sendCommand(port, argv('COMMAND', 'COUNT'));
|
|
46
|
+
const v = tryParseValue(reply, 0).value;
|
|
47
|
+
assert.strictEqual(typeof v, 'number');
|
|
48
|
+
assert.ok(Number.isInteger(v) && v > 0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('COMMAND INFO name returns doc for that command', async () => {
|
|
52
|
+
const reply = await sendCommand(port, argv('COMMAND', 'INFO', 'GET'));
|
|
53
|
+
const v = tryParseValue(reply, 0).value;
|
|
54
|
+
assert.ok(Array.isArray(v));
|
|
55
|
+
assert.ok(v.length >= 1);
|
|
56
|
+
const getDoc = v[0];
|
|
57
|
+
const name = Buffer.isBuffer(getDoc[0]) ? getDoc[0].toString('utf8') : getDoc[0];
|
|
58
|
+
assert.strictEqual(name, 'get');
|
|
59
|
+
assert.strictEqual(getDoc[1], 2, 'GET arity 2');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('COMMAND INFO unknown returns empty array', async () => {
|
|
63
|
+
const reply = await sendCommand(port, argv('COMMAND', 'INFO', 'NOSUCHCOMMAND'));
|
|
64
|
+
const v = tryParseValue(reply, 0).value;
|
|
65
|
+
assert.ok(Array.isArray(v));
|
|
66
|
+
assert.strictEqual(v.length, 0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('COMMAND unknown subcommand returns error', async () => {
|
|
70
|
+
const reply = await sendCommand(port, argv('COMMAND', 'NOSUCH'));
|
|
71
|
+
const v = tryParseValue(reply, 0).value;
|
|
72
|
+
assert.ok(v && v.error);
|
|
73
|
+
assert.match(v.error, /unknown subcommand|COMMAND/);
|
|
74
|
+
});
|
|
75
|
+
});
|