resplite 1.2.12 → 1.2.16
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 +7 -7
- package/package.json +1 -1
- package/src/commands/client.js +35 -0
- package/src/commands/registry.js +8 -0
- package/src/commands/unlink.js +12 -0
- package/src/embed.js +2 -2
- 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/README.md
CHANGED
|
@@ -63,14 +63,14 @@ const srv = await createRESPlite({
|
|
|
63
63
|
port: 6380,
|
|
64
64
|
db: './data.db',
|
|
65
65
|
hooks: {
|
|
66
|
-
onUnknownCommand({ command,
|
|
67
|
-
log.warn({ command,
|
|
66
|
+
onUnknownCommand({ command, argv, clientAddress }) {
|
|
67
|
+
log.warn({ command, argv, clientAddress }, 'unsupported command');
|
|
68
68
|
},
|
|
69
|
-
onCommandError({ command, error, clientAddress }) {
|
|
70
|
-
log.warn({ command, error, clientAddress }, 'command error');
|
|
69
|
+
onCommandError({ command, argv, error, clientAddress }) {
|
|
70
|
+
log.warn({ command, argv, error, clientAddress }, 'command error');
|
|
71
71
|
},
|
|
72
72
|
onSocketError({ error, clientAddress }) {
|
|
73
|
-
log.error({
|
|
73
|
+
log.error({ error, clientAddress }, 'connection error');
|
|
74
74
|
},
|
|
75
75
|
},
|
|
76
76
|
});
|
|
@@ -78,8 +78,8 @@ const srv = await createRESPlite({
|
|
|
78
78
|
|
|
79
79
|
Available hooks:
|
|
80
80
|
|
|
81
|
-
- `onUnknownCommand`: client sent a command not implemented by RESPLite, such as `SUBSCRIBE` or `PUBLISH`.
|
|
82
|
-
- `onCommandError`: a command failed because of wrong type, invalid args, or a handler error.
|
|
81
|
+
- `onUnknownCommand`: client sent a command not implemented by RESPLite, such as `SUBSCRIBE` or `PUBLISH`. Payload includes `argv` (full command line as strings, e.g. `['CLIENT','LIST']`) so you can log exactly what was sent.
|
|
82
|
+
- `onCommandError`: a command failed because of wrong type, invalid args, or a handler error. Payload includes `argv` for the full command line.
|
|
83
83
|
- `onSocketError`: the connection socket emitted an error, for example `ECONNRESET`.
|
|
84
84
|
|
|
85
85
|
If you want a tiny in-process smoke test that starts RESPLite and connects with the `redis` client in the same script, see [Minimal embedded example](#minimal-embedded-example) below.
|
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
|
/**
|
|
@@ -154,11 +158,13 @@ export function dispatch(engine, argv, context) {
|
|
|
154
158
|
}
|
|
155
159
|
const cmd = (Buffer.isBuffer(argv[0]) ? argv[0].toString('utf8') : String(argv[0])).toUpperCase();
|
|
156
160
|
const args = argv.slice(1);
|
|
161
|
+
const argvStrings = argv.map((b) => (Buffer.isBuffer(b) ? b.toString('utf8') : String(b)));
|
|
157
162
|
const handler = HANDLERS.get(cmd);
|
|
158
163
|
if (!handler) {
|
|
159
164
|
context?.onUnknownCommand?.({
|
|
160
165
|
command: cmd,
|
|
161
166
|
argsCount: args.length,
|
|
167
|
+
argv: argvStrings ?? [cmd],
|
|
162
168
|
clientAddress: context.clientAddress ?? '',
|
|
163
169
|
connectionId: context.connectionId ?? 0,
|
|
164
170
|
});
|
|
@@ -171,6 +177,7 @@ export function dispatch(engine, argv, context) {
|
|
|
171
177
|
context?.onCommandError?.({
|
|
172
178
|
command: cmd,
|
|
173
179
|
error: result.error,
|
|
180
|
+
argv: argvStrings ?? [cmd],
|
|
174
181
|
clientAddress: context.clientAddress ?? '',
|
|
175
182
|
connectionId: context.connectionId ?? 0,
|
|
176
183
|
});
|
|
@@ -184,6 +191,7 @@ export function dispatch(engine, argv, context) {
|
|
|
184
191
|
context?.onCommandError?.({
|
|
185
192
|
command: cmd,
|
|
186
193
|
error: errorMsg,
|
|
194
|
+
argv: argvStrings ?? [cmd],
|
|
187
195
|
clientAddress: context.clientAddress ?? '',
|
|
188
196
|
connectionId: context.connectionId ?? 0,
|
|
189
197
|
});
|
|
@@ -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/embed.js
CHANGED
|
@@ -21,8 +21,8 @@ export { handleConnection, createEngine, openDb };
|
|
|
21
21
|
* All hooks are optional. Called with plain objects; do not mutate.
|
|
22
22
|
*
|
|
23
23
|
* @typedef {object} RESPliteHooks
|
|
24
|
-
* @property {(payload: { command: string, argsCount: number, clientAddress: string, connectionId: number }) => void} [onUnknownCommand] Invoked when the client sends a command not implemented by RESPLite.
|
|
25
|
-
* @property {(payload: { command: string, error: string, clientAddress: string, connectionId: number }) => void} [onCommandError] Invoked when a command handler throws or returns an error (e.g. WRONGTYPE, invalid args).
|
|
24
|
+
* @property {(payload: { command: string, argsCount: number, argv: string[], clientAddress: string, connectionId: number }) => void} [onUnknownCommand] Invoked when the client sends a command not implemented by RESPLite. `argv` is the full command line as strings (e.g. `['CLIENT','LIST']`) for logging.
|
|
25
|
+
* @property {(payload: { command: string, error: string, argv: string[], clientAddress: string, connectionId: number }) => void} [onCommandError] Invoked when a command handler throws or returns an error (e.g. WRONGTYPE, invalid args). `argv` is the full command line as strings for logging.
|
|
26
26
|
* @property {(payload: { error: Error, clientAddress: string, connectionId: number }) => void} [onSocketError] Invoked when a connection socket emits an error (e.g. ECONNRESET).
|
|
27
27
|
*/
|
|
28
28
|
|
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'));
|