resplite 1.2.12 → 1.2.14
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 +1 -1
- package/src/commands/client.js +35 -0
- package/src/commands/registry.js +4 -0
- package/src/commands/unlink.js +12 -0
- package/src/server/connection.js +1 -0
- package/test/integration/client.test.js +103 -0
- package/test/integration/embed.test.js +5 -5
- package/test/integration/strings.test.js +11 -0
package/package.json
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLIENT - connection introspection (SETNAME, GETNAME, ID).
|
|
3
|
+
* Requires connection context.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function argStr(buf) {
|
|
7
|
+
return Buffer.isBuffer(buf) ? buf.toString('utf8') : String(buf);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function handleClient(engine, args, context) {
|
|
11
|
+
if (!args || args.length < 1) {
|
|
12
|
+
return { error: 'ERR wrong number of arguments for \'CLIENT\' command' };
|
|
13
|
+
}
|
|
14
|
+
const sub = argStr(args[0]).toUpperCase();
|
|
15
|
+
if (sub === 'SETNAME') {
|
|
16
|
+
if (args.length !== 2) {
|
|
17
|
+
return { error: 'ERR wrong number of arguments for \'CLIENT SETNAME\' command' };
|
|
18
|
+
}
|
|
19
|
+
context.connectionName = argStr(args[1]);
|
|
20
|
+
return { simple: 'OK' };
|
|
21
|
+
}
|
|
22
|
+
if (sub === 'GETNAME') {
|
|
23
|
+
if (args.length !== 1) {
|
|
24
|
+
return { error: 'ERR wrong number of arguments for \'CLIENT GETNAME\' command' };
|
|
25
|
+
}
|
|
26
|
+
return context.connectionName ?? null;
|
|
27
|
+
}
|
|
28
|
+
if (sub === 'ID') {
|
|
29
|
+
if (args.length !== 1) {
|
|
30
|
+
return { error: 'ERR wrong number of arguments for \'CLIENT ID\' command' };
|
|
31
|
+
}
|
|
32
|
+
return context.connectionId;
|
|
33
|
+
}
|
|
34
|
+
return { error: 'ERR unknown subcommand or wrong number of arguments for \'CLIENT\'. Try CLIENT HELP.' };
|
|
35
|
+
}
|
package/src/commands/registry.js
CHANGED
|
@@ -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 del from './del.js';
|
|
12
|
+
import * as unlink from './unlink.js';
|
|
12
13
|
import * as exists from './exists.js';
|
|
13
14
|
import * as type from './type.js';
|
|
14
15
|
import * as object from './object.js';
|
|
@@ -70,6 +71,7 @@ import * as ftSugadd from './ft-sugadd.js';
|
|
|
70
71
|
import * as ftSugget from './ft-sugget.js';
|
|
71
72
|
import * as ftSugdel from './ft-sugdel.js';
|
|
72
73
|
import * as monitor from './monitor.js';
|
|
74
|
+
import * as client from './client.js';
|
|
73
75
|
|
|
74
76
|
const HANDLERS = new Map([
|
|
75
77
|
['PING', (e, a) => ping.handlePing()],
|
|
@@ -78,6 +80,7 @@ const HANDLERS = new Map([
|
|
|
78
80
|
['GET', (e, a) => get.handleGet(e, a)],
|
|
79
81
|
['SET', (e, a) => set.handleSet(e, a)],
|
|
80
82
|
['DEL', (e, a) => del.handleDel(e, a)],
|
|
83
|
+
['UNLINK', (e, a) => unlink.handleUnlink(e, a)],
|
|
81
84
|
['EXISTS', (e, a) => exists.handleExists(e, a)],
|
|
82
85
|
['TYPE', (e, a) => type.handleType(e, a)],
|
|
83
86
|
['OBJECT', (e, a) => object.handleObject(e, a)],
|
|
@@ -139,6 +142,7 @@ const HANDLERS = new Map([
|
|
|
139
142
|
['FT.SUGGET', (e, a) => ftSugget.handleFtSugget(e, a)],
|
|
140
143
|
['FT.SUGDEL', (e, a) => ftSugdel.handleFtSugdel(e, a)],
|
|
141
144
|
['MONITOR', (e, a, ctx) => monitor.handleMonitor(a, ctx)],
|
|
145
|
+
['CLIENT', (e, a, ctx) => client.handleClient(e, a, ctx)],
|
|
142
146
|
]);
|
|
143
147
|
|
|
144
148
|
/**
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UNLINK key [key ...] - same as DEL; returns count of removed keys.
|
|
3
|
+
* In Redis, UNLINK is non-blocking; in RESPlite we delegate to DEL.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function handleUnlink(engine, args) {
|
|
7
|
+
if (!args || args.length < 1) {
|
|
8
|
+
return { error: 'ERR wrong number of arguments for \'UNLINK\' command' };
|
|
9
|
+
}
|
|
10
|
+
const n = engine.del(args);
|
|
11
|
+
return n;
|
|
12
|
+
}
|
package/src/server/connection.js
CHANGED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, before, after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import { createTestServer } from '../helpers/server.js';
|
|
5
|
+
import { sendCommand, argv } from '../helpers/client.js';
|
|
6
|
+
import { encode } from '../../src/resp/encoder.js';
|
|
7
|
+
import { tryParseValue } from '../../src/resp/parser.js';
|
|
8
|
+
|
|
9
|
+
function sendTwoCommands(port, argv1, argv2) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
let recv = Buffer.alloc(0);
|
|
12
|
+
const results = [];
|
|
13
|
+
const socket = net.createConnection({ port, host: '127.0.0.1' }, () => {
|
|
14
|
+
socket.write(encode(argv1));
|
|
15
|
+
socket.write(encode(argv2));
|
|
16
|
+
});
|
|
17
|
+
const t = setTimeout(() => {
|
|
18
|
+
socket.destroy();
|
|
19
|
+
reject(new Error('sendTwoCommands timeout'));
|
|
20
|
+
}, 3000);
|
|
21
|
+
socket.on('data', (chunk) => {
|
|
22
|
+
recv = Buffer.concat([recv, chunk]);
|
|
23
|
+
while (results.length < 2) {
|
|
24
|
+
const parsed = tryParseValue(recv, 0);
|
|
25
|
+
if (parsed === null) break;
|
|
26
|
+
results.push(parsed.value);
|
|
27
|
+
recv = recv.subarray(parsed.end);
|
|
28
|
+
}
|
|
29
|
+
if (results.length === 2) {
|
|
30
|
+
clearTimeout(t);
|
|
31
|
+
socket.destroy();
|
|
32
|
+
resolve(results);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
socket.on('error', (err) => {
|
|
36
|
+
clearTimeout(t);
|
|
37
|
+
reject(err);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('CLIENT integration', () => {
|
|
43
|
+
let s;
|
|
44
|
+
let port;
|
|
45
|
+
|
|
46
|
+
before(async () => {
|
|
47
|
+
s = await createTestServer();
|
|
48
|
+
port = s.port;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
after(async () => {
|
|
52
|
+
await s.closeAsync();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('CLIENT ID returns connection id', async () => {
|
|
56
|
+
const reply = await sendCommand(port, argv('CLIENT', 'ID'));
|
|
57
|
+
const v = tryParseValue(reply, 0).value;
|
|
58
|
+
assert.strictEqual(typeof v, 'number');
|
|
59
|
+
assert.ok(Number.isInteger(v) && v >= 1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('CLIENT GETNAME returns null when no name set', async () => {
|
|
63
|
+
const reply = await sendCommand(port, argv('CLIENT', 'GETNAME'));
|
|
64
|
+
const v = tryParseValue(reply, 0).value;
|
|
65
|
+
assert.strictEqual(v, null);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('CLIENT SETNAME then GETNAME roundtrip', async () => {
|
|
69
|
+
const [setResult, getResult] = await sendTwoCommands(
|
|
70
|
+
port,
|
|
71
|
+
argv('CLIENT', 'SETNAME', 'my-conn'),
|
|
72
|
+
argv('CLIENT', 'GETNAME')
|
|
73
|
+
);
|
|
74
|
+
assert.equal(setResult, 'OK');
|
|
75
|
+
assert.ok(Buffer.isBuffer(getResult));
|
|
76
|
+
assert.equal(getResult.toString('utf8'), 'my-conn');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('CLIENT SETNAME with empty name is allowed', async () => {
|
|
80
|
+
const [setResult, getResult] = await sendTwoCommands(
|
|
81
|
+
port,
|
|
82
|
+
argv('CLIENT', 'SETNAME', ''),
|
|
83
|
+
argv('CLIENT', 'GETNAME')
|
|
84
|
+
);
|
|
85
|
+
assert.equal(setResult, 'OK');
|
|
86
|
+
assert.ok(Buffer.isBuffer(getResult));
|
|
87
|
+
assert.equal(getResult.length, 0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('CLIENT wrong subcommand returns error', async () => {
|
|
91
|
+
const reply = await sendCommand(port, argv('CLIENT', 'LIST'));
|
|
92
|
+
const v = tryParseValue(reply, 0).value;
|
|
93
|
+
assert.ok(v && v.error);
|
|
94
|
+
assert.match(v.error, /unknown subcommand|CLIENT HELP/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('CLIENT without subcommand returns error', async () => {
|
|
98
|
+
const reply = await sendCommand(port, argv('CLIENT'));
|
|
99
|
+
const v = tryParseValue(reply, 0).value;
|
|
100
|
+
assert.ok(v && v.error);
|
|
101
|
+
assert.match(v.error, /wrong number of arguments/);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -147,10 +147,10 @@ describe('createRESPlite', () => {
|
|
|
147
147
|
} catch (_) {}
|
|
148
148
|
await client.quit();
|
|
149
149
|
await srv.close();
|
|
150
|
-
|
|
151
|
-
assert.
|
|
152
|
-
assert.ok(
|
|
153
|
-
assert.equal(typeof
|
|
154
|
-
assert.ok(
|
|
150
|
+
const hgetError = errorCalls.find((c) => c.command === 'HGET');
|
|
151
|
+
assert.ok(hgetError, 'expected at least one HGET error (got: ' + errorCalls.map((c) => c.command).join(', ') + ')');
|
|
152
|
+
assert.ok(hgetError.error.includes('WRONGTYPE'));
|
|
153
|
+
assert.equal(typeof hgetError.connectionId, 'number');
|
|
154
|
+
assert.ok(hgetError.clientAddress.length > 0);
|
|
155
155
|
});
|
|
156
156
|
});
|
|
@@ -33,6 +33,17 @@ describe('Strings integration', () => {
|
|
|
33
33
|
assert.equal(reply.toString('ascii'), ':1\r\n');
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
it('UNLINK returns count like DEL', async () => {
|
|
37
|
+
await sendCommand(port, argv('SET', 'u1', 'a'));
|
|
38
|
+
await sendCommand(port, argv('SET', 'u2', 'b'));
|
|
39
|
+
const one = await sendCommand(port, argv('UNLINK', 'u1'));
|
|
40
|
+
assert.equal(one.toString('ascii'), ':1\r\n');
|
|
41
|
+
const two = await sendCommand(port, argv('UNLINK', 'u2', 'u3'));
|
|
42
|
+
assert.equal(two.toString('ascii'), ':1\r\n');
|
|
43
|
+
const zero = await sendCommand(port, argv('UNLINK', 'u1'));
|
|
44
|
+
assert.equal(zero.toString('ascii'), ':0\r\n');
|
|
45
|
+
});
|
|
46
|
+
|
|
36
47
|
it('EXISTS returns count', async () => {
|
|
37
48
|
await sendCommand(port, argv('SET', 'ex1', 'a'));
|
|
38
49
|
const r = await sendCommand(port, argv('EXISTS', 'ex1', 'ex2'));
|