@yydb/yyds 0.0.14 → 0.0.15

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/bin/yyds.mjs CHANGED
@@ -1,9 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { loadYydsNative } from "../src/node/load.ts";
3
3
  import { parseStartOptions, runNodeStart } from "../src/node/runtime.ts";
4
+ import { parseWebuiArgs, runWebui } from "../src/node/webui-cmd.ts";
4
5
 
5
6
  function usage() {
6
- return "usage: yyds <init|start|status|stop> --data-dir DIR [--cluster-id ID --node-id ID] [--redis-port PORT] [--pgsql-port PORT] [--mysql-port PORT] [--mongo-port PORT] [--wire-port PORT]";
7
+ return "usage: yyds <init|start|status|stop|webui> ...";
8
+ }
9
+
10
+ function startUsage() {
11
+ return "usage: yyds start --data-dir DIR --cluster-id ID --node-id ID [--redis-port PORT] [--pgsql-port PORT] [--mysql-port PORT] [--mongo-port PORT] [--wire-port PORT]";
12
+ }
13
+
14
+ function webuiUsage() {
15
+ return "usage: yyds webui [--endpoint URL | --data-dir DIR --cluster-id ID --node-id ID [--wire-port PORT]] [--port NUMBER] [--no-open]";
7
16
  }
8
17
 
9
18
  function parseCommonArgs(command, args) {
@@ -70,7 +79,7 @@ function parseCommonArgs(command, args) {
70
79
  }
71
80
  if (!options.dataDir) throw new Error(usage());
72
81
  if ((command === "init" || command === "start") && (!options.clusterId || !options.nodeId)) {
73
- throw new Error(usage());
82
+ throw new Error(startUsage());
74
83
  }
75
84
  return options;
76
85
  }
@@ -78,6 +87,12 @@ function parseCommonArgs(command, args) {
78
87
  async function main(args) {
79
88
  const command = args[0];
80
89
  const rest = args.slice(1);
90
+
91
+ if (command === "webui") {
92
+ await runWebui(parseWebuiArgs(rest));
93
+ return;
94
+ }
95
+
81
96
  const native = loadYydsNative();
82
97
  const options = parseCommonArgs(command, rest);
83
98
 
@@ -124,7 +139,7 @@ async function main(args) {
124
139
  return;
125
140
  }
126
141
  default:
127
- throw new Error(command ? `unknown command '${command}'` : usage());
142
+ throw new Error(command ? `unknown command '${command}'` : `${usage()}\n${startUsage()}\n${webuiUsage()}`);
128
143
  }
129
144
  }
130
145
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yydb/yyds",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "description": "Node.js facade, CLI, and native bindings for local YYDS nodes.",
5
5
  "license": "MPL-2.0",
6
6
  "type": "module",
@@ -32,11 +32,13 @@
32
32
  },
33
33
  "files": [
34
34
  "bin",
35
- "src"
35
+ "src",
36
+ "webui"
36
37
  ],
37
38
  "scripts": {
39
+ "build": "node scripts/bundle-webui.mjs",
38
40
  "typecheck": "tsc --noEmit -p tsconfig.json",
39
- "test": "node --experimental-strip-types --test \"./tests/**/*.test.ts\""
41
+ "test": "node --experimental-strip-types --test \"./tests/**/*.test.ts\" \"./src/node/**/*.test.ts\""
40
42
  },
41
43
  "repository": {
42
44
  "type": "git",
@@ -57,13 +59,14 @@
57
59
  "access": "public"
58
60
  },
59
61
  "optionalDependencies": {
60
- "@yydb/yyds-win32-x64": "0.0.14",
61
- "@yydb/yyds-linux-x64": "0.0.14",
62
- "@yydb/yyds-darwin-x64": "0.0.14",
63
- "@yydb/yyds-darwin-arm64": "0.0.14"
62
+ "@yydb/yyds-win32-x64": "0.0.15",
63
+ "@yydb/yyds-linux-x64": "0.0.15",
64
+ "@yydb/yyds-darwin-x64": "0.0.15",
65
+ "@yydb/yyds-darwin-arm64": "0.0.15"
64
66
  },
65
67
  "devDependencies": {
66
68
  "@types/node": "^22.15.0",
69
+ "@yydb/yyds-webui": "0.0.15",
67
70
  "typescript": "^5.9.2"
68
71
  },
69
72
  "engines": {
package/readme.md CHANGED
@@ -10,7 +10,7 @@ Node.js facade for running a local YYDS node: native lifecycle blocks, a small C
10
10
  | `@yydb/yyds/node/cluster` | `launchYydsServer` for `redis-server`, `mysqld`, `postgres` |
11
11
  | `@yydb/yyds/node/runtime` | `runNodeStart`, `parseStartOptions` |
12
12
 
13
- The `yyds` binary provides `init`, `start`, `status`, and `stop`. Native addons for your platform are pulled in automatically as optional dependencies.
13
+ The `yyds` binary provides `init`, `start`, `status`, `stop`, and `webui`. Native addons for your platform are pulled in automatically as optional dependencies.
14
14
 
15
15
  ## Example
16
16
 
@@ -30,10 +30,11 @@ yyds init --data-dir ./node-data --cluster-id local --node-id node-1
30
30
  yyds start --data-dir ./node-data --cluster-id local --node-id node-1
31
31
  yyds status --data-dir ./node-data
32
32
  yyds stop --data-dir ./node-data
33
+ yyds webui --data-dir ./node-data --cluster-id local --node-id node-1
33
34
  ```
34
35
 
35
36
  ## Related packages
36
37
 
37
- - [`@yydb/redis`](https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/redis) �?Redis protocol disguise
38
- - [`@yydb/mysql`](https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/mysql) �?MySQL protocol disguise
39
- - [`@yydb/postgresql`](https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/postgresql) �?PostgreSQL protocol disguise
38
+ - [`@yydb/redis`](https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/redis) �?Redis protocol disguise
39
+ - [`@yydb/mysql`](https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/mysql) �?MySQL protocol disguise
40
+ - [`@yydb/postgresql`](https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/postgresql) �?PostgreSQL protocol disguise
@@ -0,0 +1,137 @@
1
+ import fs from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { ChildProcess } from "node:child_process";
4
+ import type { Server } from "node:http";
5
+
6
+ import {
7
+ openBrowser,
8
+ resolveWebuiDist,
9
+ spawnStart,
10
+ startStaticServer,
11
+ waitForWire,
12
+ wsEndpointFromWirePort,
13
+ } from "./webui-static.ts";
14
+
15
+ export interface WebuiOptions {
16
+ dataDir: string;
17
+ clusterId: string;
18
+ nodeId: string;
19
+ wirePort: number;
20
+ webuiPort: number;
21
+ endpoint?: string;
22
+ open: boolean;
23
+ }
24
+
25
+ function parsePort(raw: string | undefined, fallback: number, label: string): number {
26
+ const value = Number(raw ?? fallback);
27
+ if (!Number.isInteger(value) || value < 1 || value > 65535) {
28
+ throw new Error(`invalid ${label}: ${raw ?? fallback}`);
29
+ }
30
+ return value;
31
+ }
32
+
33
+ export function parseWebuiArgs(args: string[]): WebuiOptions {
34
+ const options: WebuiOptions = {
35
+ dataDir: "",
36
+ clusterId: "",
37
+ nodeId: "",
38
+ wirePort: 7700,
39
+ webuiPort: 8780,
40
+ open: true,
41
+ };
42
+ for (let index = 0; index < args.length; index += 1) {
43
+ const flag = args[index];
44
+ const value = args[index + 1];
45
+ switch (flag) {
46
+ case "--data-dir":
47
+ options.dataDir = value ?? "";
48
+ index += 1;
49
+ break;
50
+ case "--cluster-id":
51
+ options.clusterId = value ?? "";
52
+ index += 1;
53
+ break;
54
+ case "--node-id":
55
+ options.nodeId = value ?? "";
56
+ index += 1;
57
+ break;
58
+ case "--wire-port":
59
+ options.wirePort = parsePort(value, options.wirePort, "wire-port");
60
+ index += 1;
61
+ break;
62
+ case "--port":
63
+ options.webuiPort = parsePort(value, options.webuiPort, "port");
64
+ index += 1;
65
+ break;
66
+ case "--endpoint":
67
+ options.endpoint = value ?? "";
68
+ index += 1;
69
+ break;
70
+ case "--no-open":
71
+ options.open = false;
72
+ break;
73
+ default:
74
+ throw new Error(`unknown option '${flag}'`);
75
+ }
76
+ }
77
+ if (!options.endpoint && (!options.dataDir || !options.clusterId || !options.nodeId)) {
78
+ throw new Error(
79
+ "webui requires --endpoint or --data-dir with --cluster-id and --node-id",
80
+ );
81
+ }
82
+ return options;
83
+ }
84
+
85
+ export async function runWebui(options: WebuiOptions): Promise<void> {
86
+ const endpoint = options.endpoint ?? wsEndpointFromWirePort(options.wirePort);
87
+ const distRoot = resolveWebuiDist();
88
+ if (!fs.existsSync(join(distRoot, "index.html"))) {
89
+ throw new Error(
90
+ "WebUI assets not found. Run `pnpm --filter @yydb/yyds build` to bundle webui/.",
91
+ );
92
+ }
93
+
94
+ let startChild: ChildProcess | null = null;
95
+ if (!options.endpoint) {
96
+ startChild = spawnStart({
97
+ dataDir: options.dataDir,
98
+ clusterId: options.clusterId,
99
+ nodeId: options.nodeId,
100
+ wirePort: options.wirePort,
101
+ });
102
+ startChild.stdout?.on("data", (chunk) => process.stdout.write(chunk));
103
+ startChild.stderr?.on("data", (chunk) => process.stderr.write(chunk));
104
+ startChild.on("exit", (code) => {
105
+ if (code && code !== 0) {
106
+ console.error(`yyds start exited with code ${code}`);
107
+ }
108
+ });
109
+ await waitForWire(endpoint, 8000);
110
+ }
111
+
112
+ const server = await startStaticServer(distRoot, options.webuiPort);
113
+ const pageUrl = `http://127.0.0.1:${options.webuiPort}/?endpoint=${encodeURIComponent(endpoint)}`;
114
+
115
+ console.log(`WebUI ${pageUrl}`);
116
+ console.log(`Wire ${endpoint}`);
117
+
118
+ if (options.open) {
119
+ openBrowser(pageUrl);
120
+ }
121
+
122
+ await waitForShutdown(server, startChild);
123
+ }
124
+
125
+ function waitForShutdown(server: Server, startChild: ChildProcess | null): Promise<void> {
126
+ return new Promise((resolveWait) => {
127
+ const shutdown = () => {
128
+ server.close();
129
+ if (startChild && !startChild.killed) {
130
+ startChild.kill();
131
+ }
132
+ resolveWait();
133
+ };
134
+ process.once("SIGINT", shutdown);
135
+ process.once("SIGTERM", shutdown);
136
+ });
137
+ }
@@ -0,0 +1,12 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { wsEndpointFromWirePort } from "./webui-static.ts";
5
+
6
+ test("wsEndpointFromWirePort maps loopback wire port to WebSocket URL", () => {
7
+ assert.equal(wsEndpointFromWirePort(7700), "ws://127.0.0.1:7700/wire");
8
+ });
9
+
10
+ test("wsEndpointFromWirePort accepts custom host", () => {
11
+ assert.equal(wsEndpointFromWirePort(9001, "10.0.0.5"), "ws://10.0.0.5:9001/wire");
12
+ });
@@ -0,0 +1,137 @@
1
+ import { createRequire } from "node:module";
2
+ import { createServer, type Server } from "node:http";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { spawn, type ChildProcess } from "node:child_process";
7
+
8
+ const MIME: Record<string, string> = {
9
+ ".html": "text/html; charset=utf-8",
10
+ ".js": "text/javascript; charset=utf-8",
11
+ ".css": "text/css; charset=utf-8",
12
+ ".json": "application/json; charset=utf-8",
13
+ ".svg": "image/svg+xml",
14
+ ".woff2": "font/woff2",
15
+ ".png": "image/png",
16
+ ".ico": "image/x-icon",
17
+ };
18
+
19
+ function contentType(path: string): string {
20
+ const extension = path.slice(path.lastIndexOf("."));
21
+ return MIME[extension] ?? "application/octet-stream";
22
+ }
23
+
24
+ export function resolveWebuiDist(): string {
25
+ const bundled = join(dirname(fileURLToPath(import.meta.url)), "../../webui");
26
+ if (existsSync(join(bundled, "index.html"))) {
27
+ return bundled;
28
+ }
29
+ try {
30
+ const require = createRequire(import.meta.url);
31
+ const pkgJson = require.resolve("@yydb/yyds-webui/package.json");
32
+ const external = join(dirname(pkgJson), "dist", "cdn");
33
+ if (existsSync(join(external, "index.html"))) {
34
+ return external;
35
+ }
36
+ } catch {
37
+ // @yydb/yyds-webui not installed
38
+ }
39
+ return bundled;
40
+ }
41
+
42
+ export function wsEndpointFromWirePort(wirePort: number, host = "127.0.0.1"): string {
43
+ return `ws://${host}:${wirePort}/wire`;
44
+ }
45
+
46
+ export function startStaticServer(root: string, port: number): Promise<Server> {
47
+ const absRoot = resolve(root);
48
+ return new Promise((resolvePromise, reject) => {
49
+ const server = createServer((request, response) => {
50
+ const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://localhost").pathname);
51
+ const relative = pathname === "/" ? "index.html" : pathname.replace(/^\//, "");
52
+ const target = resolve(absRoot, relative);
53
+ if (!target.startsWith(absRoot)) {
54
+ response.writeHead(403);
55
+ response.end("forbidden");
56
+ return;
57
+ }
58
+ try {
59
+ const bytes = readFileSync(target);
60
+ response.writeHead(200, { "Content-Type": contentType(target) });
61
+ response.end(bytes);
62
+ } catch {
63
+ response.writeHead(404);
64
+ response.end("not found");
65
+ }
66
+ });
67
+ server.listen(port, "127.0.0.1", () => resolvePromise(server));
68
+ server.on("error", reject);
69
+ });
70
+ }
71
+
72
+ export interface StartSpawnOptions {
73
+ dataDir: string;
74
+ clusterId: string;
75
+ nodeId: string;
76
+ wirePort: number;
77
+ }
78
+
79
+ export function spawnStart(options: StartSpawnOptions): ChildProcess {
80
+ const cli = fileURLToPath(new URL("../../bin/yyds.mjs", import.meta.url));
81
+ const args = [
82
+ cli,
83
+ "start",
84
+ "--data-dir",
85
+ options.dataDir,
86
+ "--cluster-id",
87
+ options.clusterId,
88
+ "--node-id",
89
+ options.nodeId,
90
+ "--wire-port",
91
+ String(options.wirePort),
92
+ ];
93
+ return spawn(process.execPath, args, {
94
+ stdio: ["ignore", "pipe", "pipe"],
95
+ env: process.env,
96
+ });
97
+ }
98
+
99
+ export function openBrowser(url: string): void {
100
+ const platform = process.platform;
101
+ if (platform === "win32") {
102
+ spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" });
103
+ return;
104
+ }
105
+ const opener = platform === "darwin" ? "open" : "xdg-open";
106
+ spawn(opener, [url], { detached: true, stdio: "ignore" });
107
+ }
108
+
109
+ export function sleep(ms: number): Promise<void> {
110
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
111
+ }
112
+
113
+ export async function waitForWire(endpoint: string, timeoutMs = 8000): Promise<void> {
114
+ const deadline = Date.now() + timeoutMs;
115
+ while (Date.now() < deadline) {
116
+ try {
117
+ const socket = new WebSocket(endpoint);
118
+ const opened = await new Promise<boolean>((resolveOpen) => {
119
+ const timer = setTimeout(() => resolveOpen(false), 500);
120
+ socket.addEventListener("open", () => {
121
+ clearTimeout(timer);
122
+ resolveOpen(true);
123
+ });
124
+ socket.addEventListener("error", () => {
125
+ clearTimeout(timer);
126
+ resolveOpen(false);
127
+ });
128
+ });
129
+ socket.close();
130
+ if (opened) return;
131
+ } catch {
132
+ await sleep(400);
133
+ return;
134
+ }
135
+ await sleep(200);
136
+ }
137
+ }