@yydb/redis 0.0.1 → 0.0.2

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.
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { createConnection } from "node:net";
4
+
5
+ const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
6
+ const maxRequestBytes = 1024 * 1024;
7
+ const maxReplyBytes = 8 * 1024 * 1024;
8
+
9
+ function parseValue(buffer, offset = 0, depth = 0) {
10
+ if (depth > 32) throw new Error("RESP response exceeds nesting limit");
11
+ if (offset >= buffer.length) return null;
12
+ const marker = buffer[offset];
13
+ const lineEnd = buffer.indexOf("\r\n", offset + 1);
14
+ if (lineEnd < 0) return null;
15
+ const line = buffer.toString("utf8", offset + 1, lineEnd);
16
+ const next = lineEnd + 2;
17
+ if (marker === 43 || marker === 45)
18
+ return { value: { kind: marker === 43 ? "string" : "error", value: line }, next };
19
+ if (marker === 58) {
20
+ if (!/^-?\d+$/.test(line)) throw new Error("invalid RESP integer response");
21
+ const integer = BigInt(line);
22
+ if (integer < -(1n << 63n) || integer >= 1n << 63n)
23
+ throw new Error("RESP integer outside signed 64-bit range");
24
+ return { value: { kind: "integer", value: line }, next };
25
+ }
26
+ if (marker === 36) {
27
+ if (!/^(?:-1|\d+)$/.test(line)) throw new Error("invalid RESP bulk length");
28
+ const length = Number(line);
29
+ if (!Number.isSafeInteger(length) || length < -1)
30
+ throw new Error("invalid RESP bulk length");
31
+ if (length === -1) return { value: { kind: "null" }, next };
32
+ if (length > maxReplyBytes) throw new Error("RESP bulk reply exceeds byte limit");
33
+ const end = next + length;
34
+ if (buffer.length < end + 2) return null;
35
+ if (buffer[end] !== 13 || buffer[end + 1] !== 10)
36
+ throw new Error("invalid RESP bulk terminator");
37
+ return { value: { kind: "bulk", value: buffer.subarray(next, end) }, next: end + 2 };
38
+ }
39
+ if (marker === 42) {
40
+ if (!/^(?:-1|\d+)$/.test(line)) throw new Error("invalid RESP array length");
41
+ const count = Number(line);
42
+ if (!Number.isSafeInteger(count) || count < -1 || count > 1024)
43
+ throw new Error("invalid RESP array length");
44
+ if (count === -1) return { value: { kind: "null" }, next };
45
+ const values = [];
46
+ let cursor = next;
47
+ for (let index = 0; index < count; index += 1) {
48
+ const parsed = parseValue(buffer, cursor, depth + 1);
49
+ if (parsed === null) return null;
50
+ values.push(parsed.value);
51
+ cursor = parsed.next;
52
+ }
53
+ return { value: { kind: "array", value: values }, next: cursor };
54
+ }
55
+ throw new Error("unsupported RESP2 response type");
56
+ }
57
+
58
+ function encodeCommand(args) {
59
+ const values = args.map((value) => Buffer.from(value, "utf8"));
60
+ const encoded = Buffer.concat([
61
+ Buffer.from(`*${values.length}\r\n`),
62
+ ...values.flatMap((value) => [
63
+ Buffer.from(`$${value.length}\r\n`),
64
+ value,
65
+ Buffer.from("\r\n"),
66
+ ]),
67
+ ]);
68
+ if (encoded.length > maxRequestBytes) throw new Error("Redis command exceeds byte limit");
69
+ return encoded;
70
+ }
71
+
72
+ function writeValue(value, raw) {
73
+ if (value.kind === "error") throw new Error(value.value);
74
+ if (value.kind === "null") {
75
+ process.stdout.write("(nil)\n");
76
+ return;
77
+ }
78
+ if (value.kind === "array") {
79
+ for (const item of value.value) writeValue(item, raw);
80
+ return;
81
+ }
82
+ if (value.kind === "bulk") {
83
+ process.stdout.write(raw ? value.value : value.value.toString("utf8"));
84
+ process.stdout.write("\n");
85
+ return;
86
+ }
87
+ process.stdout.write(`${value.value}\n`);
88
+ }
89
+
90
+ function parseArguments(args) {
91
+ const options = { host: "127.0.0.1", port: 6379, database: 0, raw: false };
92
+ const command = [];
93
+ for (let index = 0; index < args.length; index += 1) {
94
+ const arg = args[index];
95
+ if (command.length > 0) {
96
+ command.push(arg);
97
+ continue;
98
+ }
99
+ if (arg === "--raw") options.raw = true;
100
+ else if (["-h", "--host", "-p", "--port", "-n", "--db"].includes(arg)) {
101
+ const value = args[++index];
102
+ if (value === undefined) throw new Error(`missing value for ${arg}`);
103
+ if (arg === "-h" || arg === "--host") options.host = value;
104
+ else {
105
+ const parsed = Number(value);
106
+ const minimum = arg === "-n" || arg === "--db" ? 0 : 1;
107
+ if (
108
+ !Number.isInteger(parsed) ||
109
+ parsed < minimum ||
110
+ parsed > (arg === "-n" || arg === "--db" ? 15 : 65535)
111
+ ) {
112
+ throw new Error(`invalid value for ${arg}`);
113
+ }
114
+ if (arg === "-p" || arg === "--port") options.port = parsed;
115
+ else options.database = parsed;
116
+ }
117
+ } else if (arg.startsWith("-")) throw new Error(`unsupported option '${arg}'`);
118
+ else command.push(arg);
119
+ }
120
+ if (command.length === 0) command.push("PING");
121
+ return { options, command };
122
+ }
123
+
124
+ function exchange(options, commands) {
125
+ return new Promise((resolve, reject) => {
126
+ const socket = createConnection({ host: options.host, port: options.port });
127
+ let pending = Buffer.alloc(0);
128
+ let commandIndex = 0;
129
+ let settled = false;
130
+ let timer;
131
+ const allCommands =
132
+ options.database === 0 ? [commands] : [["SELECT", String(options.database)], commands];
133
+ const finish = (error, value) => {
134
+ if (settled) return;
135
+ settled = true;
136
+ clearTimeout(timer);
137
+ socket.destroy();
138
+ if (error) reject(error);
139
+ else resolve(value);
140
+ };
141
+ timer = setTimeout(() => finish(new Error("Redis connection timed out")), 5000);
142
+ socket.on("connect", () => {
143
+ try {
144
+ socket.write(encodeCommand(allCommands[commandIndex]));
145
+ } catch (error) {
146
+ finish(error);
147
+ }
148
+ });
149
+ socket.on("data", (chunk) => {
150
+ try {
151
+ pending = Buffer.concat([pending, chunk]);
152
+ if (pending.length > maxReplyBytes)
153
+ throw new Error("RESP reply exceeds byte limit");
154
+ const parsed = parseValue(pending);
155
+ if (parsed === null) return;
156
+ pending = pending.subarray(parsed.next);
157
+ if (commandIndex === 0 && options.database !== 0) {
158
+ if (parsed.value.kind === "error") throw new Error(parsed.value.value);
159
+ if (parsed.value.kind !== "string" || parsed.value.value !== "OK") {
160
+ throw new Error("Redis SELECT did not return OK");
161
+ }
162
+ commandIndex += 1;
163
+ socket.write(encodeCommand(allCommands[commandIndex]));
164
+ return;
165
+ }
166
+ finish(null, parsed.value);
167
+ } catch (error) {
168
+ finish(error);
169
+ }
170
+ });
171
+ socket.on("error", (error) => finish(error));
172
+ socket.on("end", () => finish(new Error("Redis closed before a complete reply")));
173
+ });
174
+ }
175
+
176
+ async function main(args) {
177
+ if (args.length === 1 && ["--version", "-V"].includes(args[0])) {
178
+ console.log(`redis-cli ${version} (YYDS RESP2 client)`);
179
+ return;
180
+ }
181
+ if (args.length === 1 && args[0] === "--help") {
182
+ console.log("Usage: redis-cli [-h HOST] [-p PORT] [-n DB] [--raw] [COMMAND [ARG ...]]");
183
+ return;
184
+ }
185
+ const { options, command } = parseArguments(args);
186
+ writeValue(await exchange(options, command), options.raw);
187
+ }
188
+
189
+ main(process.argv.slice(2)).catch((error) => {
190
+ console.error(`@yydb/redis: ${error.message}`);
191
+ process.exitCode = 1;
192
+ });
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { launchYydsServer } from "@yydb/yyds/node/cluster";
3
+
4
+ const args = process.argv.slice(2);
5
+ if (args.length === 1 && args[0] === "--help") {
6
+ console.log("Usage: redis-server --data-dir DIR --cluster-id ID --node-id ID [--port PORT]");
7
+ console.log("Starts the YYDS node and its protocol listener through the yyds cluster tool.");
8
+ } else {
9
+ launchYydsServer("redis", args)
10
+ .then((code) => {
11
+ process.exitCode = code;
12
+ })
13
+ .catch((error) => {
14
+ console.error("@yydb/redis: " + error.message);
15
+ process.exitCode = 127;
16
+ });
17
+ }
package/package.json CHANGED
@@ -1,34 +1,60 @@
1
1
  {
2
2
  "name": "@yydb/redis",
3
- "version": "0.0.1",
4
- "description": "Redis driver and workbench surfaces for SQL Studio — 0.0.1 Developer Preview",
5
- "type": "module",
3
+ "version": "0.0.2",
4
+ "description": "Pretend you installed Redis. YYDS scripting runtime underneath.",
6
5
  "license": "MPL-2.0",
6
+ "type": "module",
7
7
  "sideEffects": false,
8
- "main": "./src/index.ts",
9
- "types": "./src/index.ts",
10
8
  "exports": {
11
9
  ".": {
12
10
  "types": "./src/index.ts",
13
11
  "import": "./src/index.ts",
14
12
  "default": "./src/index.ts"
13
+ },
14
+ "./node": {
15
+ "types": "./src/node.ts",
16
+ "import": "./src/node.ts",
17
+ "default": "./src/node.ts"
15
18
  }
16
19
  },
20
+ "bin": {
21
+ "redis-server": "./bin/redis-server.mjs",
22
+ "redis-cli": "./bin/redis-cli.mjs"
23
+ },
17
24
  "files": [
25
+ "bin",
18
26
  "src",
19
- "README.md"
27
+ "readme.md"
20
28
  ],
21
29
  "scripts": {
22
- "typecheck": "tsc --noEmit -p tsconfig.json"
30
+ "typecheck": "tsc --noEmit -p tsconfig.json",
31
+ "test": "node --experimental-strip-types --test \"./tests/**/*.test.ts\""
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/yy-database/yyds.git",
36
+ "directory": "projects/packages/redis"
37
+ },
38
+ "homepage": "https://github.com/yy-database/yyds/tree/dev/projects/packages/redis#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/yy-database/yyds/issues"
41
+ },
42
+ "keywords": [
43
+ "yyds",
44
+ "redis",
45
+ "disguise"
46
+ ],
47
+ "publishConfig": {
48
+ "access": "public"
23
49
  },
24
50
  "dependencies": {
25
- "@yydb/sql-studio-protocol": "0.0.1"
51
+ "@yydb/yyds": "0.0.2"
26
52
  },
27
53
  "devDependencies": {
54
+ "@types/node": "^22.15.0",
28
55
  "typescript": "^5.9.2"
29
56
  },
30
- "publishConfig": {
31
- "access": "public",
32
- "tag": "dev"
57
+ "engines": {
58
+ "node": ">=18"
33
59
  }
34
60
  }
package/readme.md ADDED
@@ -0,0 +1,26 @@
1
+ # @yyds/redis
2
+
3
+ `redis-cli` is a RESP2 client for a running YYDS Redis protocol service. The service lifecycle belongs to the `yyds` tool, which starts the node and its protocol listener.
4
+
5
+ ```bash
6
+ redis-cli PING
7
+ # PONG
8
+
9
+ redis-cli -h 127.0.0.1 -p 6379 SET key value
10
+ redis-cli -h 127.0.0.1 -p 6379 SET key value NX
11
+ redis-cli -h 127.0.0.1 -p 6379 SET key replacement XX
12
+ redis-cli -h 127.0.0.1 -p 6379 EXISTS key
13
+ redis-cli -h 127.0.0.1 -p 6379 GETDEL key
14
+ redis-cli -h 127.0.0.1 -p 6379 GETSET key replacement
15
+ redis-cli -h 127.0.0.1 -p 6379 INCR visits
16
+ redis-cli -h 127.0.0.1 -p 6379 INCRBY visits -2
17
+ redis-cli -h 127.0.0.1 -p 6379 DECRBY visits 2
18
+ redis-cli -h 127.0.0.1 -p 6379 MGET key missing key
19
+ redis-cli -n 1 GET key
20
+ ```
21
+
22
+ The node-owned service supports binary `GET`, ordered atomic `MGET`, single-key `EXISTS`, atomic `GETDEL` and `GETSET`, `SET`, `SET ... NX` and `SET ... XX`, single-key `DEL`, and signed 64-bit `INCR`/`INCRBY`/`DECR`/`DECRBY`. MGET preserves request order and duplicate keys. Conditional set, get-and-delete, get-and-replace, increments/decrements and MGET are atomic within the current single-process executor and persisted by its mounted shard. Missing integer keys start at zero. Multi-key `EXISTS` and other `SET` options remain unsupported. These operations do not imply replicated or multi-node atomicity. The client supports one RESP2 command per invocation, host/port/database selection, simple strings, errors, integers, bulk values and arrays. It does not provide authentication, TLS, RESP3, interactive mode or full `redis-cli` compatibility. Server lifecycle is provided by `yyds`, not a second `redis-server` manager.
23
+
24
+ `redis-server` delegates to the `yyds` executable and holds the node lifecycle. Pass `--data-dir`, `--cluster-id`, and `--node-id`; set `YYDS_CLI_PATH` when the executable is not on `PATH`. The alias does not create a Redis-only supervisor.
25
+
26
+ The package API re-exports `@yyds/yyds` unchanged.
@@ -0,0 +1,23 @@
1
+ import { loadYydsNative } from "@yydb/yyds/node";
2
+
3
+ export type DisguiseRuntime = {
4
+ product: string;
5
+ yydsVersion: string;
6
+ ping: string;
7
+ };
8
+
9
+ export function loadDisguiseRuntime(product: string): DisguiseRuntime {
10
+ try {
11
+ const native = loadYydsNative();
12
+ return {
13
+ product,
14
+ yydsVersion: native.yydsVersion(),
15
+ ping: native.ping(),
16
+ };
17
+ } catch (error) {
18
+ throw new Error(
19
+ `@yydb/${product}: native runtime missing. Install @yydb/yyds and run pnpm build:napi`,
20
+ { cause: error },
21
+ );
22
+ }
23
+ }
package/src/index.ts CHANGED
@@ -1,21 +1 @@
1
- /**
2
- * `@yydb/redis` — Redis driver for SQL Studio (Key Browser / Command Console).
3
- * Product brand is SQL Studio; Redis is a Data Source, not a SQL engine.
4
- */
5
-
6
- import type { StudioDriverRegistration } from "@yydb/sql-studio-protocol";
7
-
8
- export const DRIVER_ID = "redis" as const;
9
-
10
- export type RedisFactoryOptions = {
11
- id: string;
12
- url: string;
13
- };
14
-
15
- export function redis(options: RedisFactoryOptions): StudioDriverRegistration {
16
- return {
17
- id: options.id,
18
- kind: "redis",
19
- driver: { url: options.url, kind: DRIVER_ID },
20
- };
21
- }
1
+ export * from "@yydb/yyds";
package/src/node.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "@yydb/yyds/node";
package/README.md DELETED
@@ -1,4 +0,0 @@
1
- # 🔴 `@yydb/redis`
2
-
3
- Redis driver under the **SQL Studio** brand. Studio UI uses Key Browser /
4
- Command Console for this Data Source.