-
Notifications
You must be signed in to change notification settings - Fork 0
/
mod.ts
108 lines (99 loc) · 2.92 KB
/
mod.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import { BufReader, TextProtoReader, writeAll } from "./deps.ts";
export type Command = (string | number)[];
export type Reply = string | number | null | Reply[];
const CRLF = "\r\n";
const encoder = new TextEncoder();
function removePrefix(line: string): string {
return line.slice(1);
}
/**
* Transforms a command, which is an array of arguments, into an RESP request string.
* @see https://redis.io/docs/reference/protocol-spec/#send-commands-to-a-redis-server
*/
function stringifyRequest(command: Command): string {
let request = "*" + command.length + CRLF;
for (const arg of command) {
request += "$" + arg.toString().length + CRLF;
request += arg + CRLF;
}
return request;
}
/** Encodes and sends the request string to the server. */
async function writeRequest(
conn: Deno.Conn,
request: string,
): Promise<void> {
await writeAll(conn, encoder.encode(request));
}
async function readNonNullArray(
tpReader: TextProtoReader,
length: number,
): Promise<Reply[]> {
const array = [];
for (let i = 0; i < length; i++) {
const reply: Reply = await readReply(tpReader);
array.push(reply);
}
return array;
}
/**
* Reads and processes the response line-by-line.
* @see https://redis.io/docs/reference/protocol-spec/#resp-protocol-description
*/
async function readReply(tpReader: TextProtoReader): Promise<Reply> {
const line = await tpReader.readLine();
switch (line!.charAt(0)) {
/** Simple string */
case "+":
return removePrefix(line!);
/** Error */
case "-":
return Promise.reject(removePrefix(line!));
/** Integer */
case ":":
return Number(removePrefix(line!));
/** Bulk string */
case "$":
return Number(removePrefix(line!)) === -1
? null
: /** Skip to reading the next line, which is a string */
await readReply(tpReader);
/** Array */
case "*": {
const length = Number(removePrefix(line!));
return length === -1 ? null : await readNonNullArray(tpReader, length);
}
/** No prefix */
default:
return line;
}
}
function toTpReader(redisConn: Deno.Conn): TextProtoReader {
return new TextProtoReader(new BufReader(redisConn));
}
/**
* Sends a command to the Redis server and returns the parsed reply.
*
* Example:
* ```ts
* import { sendCommand } from "https://deno.land/x/r2d2/mod.ts";
*
* const redisConn = await Deno.connect({ port: 6379 });
*
* // Resolves to "OK"
* await sendCommand(redisConn, ["SET", "hello", "world"]);
*
* // Prints "world"
* console.log(await sendCommand(redisConn, ["GET", "hello"]));
* ```
* @param redisConn Redis connection to the server.
* @param command Redis command, which is an array of arguments.
* @returns Parsed Redis reply
*/
export async function sendCommand(
redisConn: Deno.Conn,
command: Command,
): Promise<Reply> {
await writeRequest(redisConn, stringifyRequest(command));
return await readReply(toTpReader(redisConn));
}