@yydb/yyds 0.0.13 → 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,13 +1,31 @@
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] [--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) {
10
- const options = { dataDir: "", clusterId: "", nodeId: "", redisPort: 6379, pgsqlPort: undefined, mysqlPort: undefined, wirePort: undefined };
19
+ const options = {
20
+ dataDir: "",
21
+ clusterId: "",
22
+ nodeId: "",
23
+ redisPort: 6379,
24
+ pgsqlPort: undefined,
25
+ mysqlPort: undefined,
26
+ mongoPort: undefined,
27
+ wirePort: undefined,
28
+ };
11
29
  for (let index = 0; index < args.length; index += 1) {
12
30
  const flag = args[index];
13
31
  const value = args[index + 1];
@@ -26,22 +44,32 @@ function parseCommonArgs(command, args) {
26
44
  index += 1;
27
45
  break;
28
46
  case "--redis-port":
29
- if (command !== "start") throw new Error(`option ${flag} is not valid for this command`);
47
+ if (command !== "start")
48
+ throw new Error(`option ${flag} is not valid for this command`);
30
49
  options.redisPort = Number(value);
31
50
  index += 1;
32
51
  break;
33
52
  case "--pgsql-port":
34
- if (command !== "start") throw new Error(`option ${flag} is not valid for this command`);
53
+ if (command !== "start")
54
+ throw new Error(`option ${flag} is not valid for this command`);
35
55
  options.pgsqlPort = Number(value);
36
56
  index += 1;
37
57
  break;
38
58
  case "--mysql-port":
39
- if (command !== "start") throw new Error(`option ${flag} is not valid for this command`);
59
+ if (command !== "start")
60
+ throw new Error(`option ${flag} is not valid for this command`);
40
61
  options.mysqlPort = Number(value);
41
62
  index += 1;
42
63
  break;
64
+ case "--mongo-port":
65
+ if (command !== "start")
66
+ throw new Error(`option ${flag} is not valid for this command`);
67
+ options.mongoPort = Number(value);
68
+ index += 1;
69
+ break;
43
70
  case "--wire-port":
44
- if (command !== "start") throw new Error(`option ${flag} is not valid for this command`);
71
+ if (command !== "start")
72
+ throw new Error(`option ${flag} is not valid for this command`);
45
73
  options.wirePort = Number(value);
46
74
  index += 1;
47
75
  break;
@@ -51,7 +79,7 @@ function parseCommonArgs(command, args) {
51
79
  }
52
80
  if (!options.dataDir) throw new Error(usage());
53
81
  if ((command === "init" || command === "start") && (!options.clusterId || !options.nodeId)) {
54
- throw new Error(usage());
82
+ throw new Error(startUsage());
55
83
  }
56
84
  return options;
57
85
  }
@@ -59,6 +87,12 @@ function parseCommonArgs(command, args) {
59
87
  async function main(args) {
60
88
  const command = args[0];
61
89
  const rest = args.slice(1);
90
+
91
+ if (command === "webui") {
92
+ await runWebui(parseWebuiArgs(rest));
93
+ return;
94
+ }
95
+
62
96
  const native = loadYydsNative();
63
97
  const options = parseCommonArgs(command, rest);
64
98
 
@@ -77,18 +111,21 @@ async function main(args) {
77
111
  redisPort: options.redisPort,
78
112
  pgsqlPort: options.pgsqlPort,
79
113
  mysqlPort: options.mysqlPort,
114
+ mongoPort: options.mongoPort,
80
115
  wirePort: options.wirePort,
81
116
  });
82
117
  return;
83
118
  case "status": {
84
119
  const snapshot = native.nodeInspect(options.dataDir);
85
- console.log(JSON.stringify({
86
- directory: snapshot.directory,
87
- identity: JSON.parse(snapshot.identity),
88
- ownership: snapshot.ownership,
89
- runtime: snapshot.runtime ? JSON.parse(snapshot.runtime) : null,
90
- health: snapshot.health,
91
- }));
120
+ console.log(
121
+ JSON.stringify({
122
+ directory: snapshot.directory,
123
+ identity: JSON.parse(snapshot.identity),
124
+ ownership: snapshot.ownership,
125
+ runtime: snapshot.runtime ? JSON.parse(snapshot.runtime) : null,
126
+ health: snapshot.health,
127
+ }),
128
+ );
92
129
  return;
93
130
  }
94
131
  case "stop": {
@@ -102,7 +139,7 @@ async function main(args) {
102
139
  return;
103
140
  }
104
141
  default:
105
- throw new Error(command ? `unknown command '${command}'` : usage());
142
+ throw new Error(command ? `unknown command '${command}'` : `${usage()}\n${startUsage()}\n${webuiUsage()}`);
106
143
  }
107
144
  }
108
145
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yydb/yyds",
3
- "version": "0.0.13",
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,18 +32,20 @@
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",
43
45
  "url": "git+https://github.com/yy-database/yyds.git",
44
- "directory": "projects/packages/yyds"
46
+ "directory": "projects/yyds.ts/packages/yyds"
45
47
  },
46
- "homepage": "https://github.com/yy-database/yyds/tree/dev/projects/packages/yyds#readme",
48
+ "homepage": "https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/yyds#readme",
47
49
  "bugs": {
48
50
  "url": "https://github.com/yy-database/yyds/issues"
49
51
  },
@@ -57,13 +59,14 @@
57
59
  "access": "public"
58
60
  },
59
61
  "optionalDependencies": {
60
- "@yydb/yyds-win32-x64": "0.0.13",
61
- "@yydb/yyds-linux-x64": "0.0.13",
62
- "@yydb/yyds-darwin-x64": "0.0.13",
63
- "@yydb/yyds-darwin-arm64": "0.0.13"
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/packages/redis) — Redis protocol disguise
38
- - [`@yydb/mysql`](https://github.com/yy-database/yyds/tree/dev/projects/packages/mysql) — MySQL protocol disguise
39
- - [`@yydb/postgresql`](https://github.com/yy-database/yyds/tree/dev/projects/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
package/src/bindings.ts CHANGED
@@ -7,9 +7,18 @@ export interface YydsBindings {
7
7
  acquire(dataDir: string, identity: unknown): unknown;
8
8
  };
9
9
  LocalExecutorHandle: new (dataDir: string) => unknown;
10
- RedisServiceHandle: new (lease: unknown, port: number, executor: unknown) => RedisServiceBinding;
10
+ RedisServiceHandle: new (
11
+ lease: unknown,
12
+ port: number,
13
+ executor: unknown,
14
+ ) => RedisServiceBinding;
11
15
  PgsqlServiceHandle: new (lease: unknown, port: number) => PgsqlServiceBinding;
12
16
  MysqlServiceHandle: new (lease: unknown, port: number) => MysqlServiceBinding;
17
+ MongoServiceHandle: new (
18
+ lease: unknown,
19
+ port: number,
20
+ executor: unknown,
21
+ ) => MongoServiceBinding;
13
22
  WireServerHandle: new (port: number, executor: unknown) => WireServiceBinding;
14
23
  nodeInit(dataDir: string, identity: unknown): void;
15
24
  nodeInspect(dataDir: string): NodeStatusBinding;
@@ -30,6 +39,7 @@ export interface ServiceBinding {
30
39
  export type RedisServiceBinding = ServiceBinding;
31
40
  export type PgsqlServiceBinding = ServiceBinding;
32
41
  export type MysqlServiceBinding = ServiceBinding;
42
+ export type MongoServiceBinding = ServiceBinding;
33
43
  export type WireServiceBinding = ServiceBinding;
34
44
 
35
45
  export interface NodeStatusBinding {
@@ -12,6 +12,7 @@ export interface StartNodeOptions {
12
12
  redisPort?: number;
13
13
  pgsqlPort?: number;
14
14
  mysqlPort?: number;
15
+ mongoPort?: number;
15
16
  wirePort?: number;
16
17
  }
17
18
 
@@ -33,7 +34,8 @@ export function parseStartOptions(protocol: YydsProtocol, args: string[]): Start
33
34
  const argument = args[index];
34
35
  if (["--port", "-p", "-P"].includes(argument)) {
35
36
  const value = Number(args[++index]);
36
- if (!Number.isInteger(value) || value < 0) throw new Error(`missing port after ${argument}`);
37
+ if (!Number.isInteger(value) || value < 0)
38
+ throw new Error(`missing port after ${argument}`);
37
39
  if (protocol === "redis") options.redisPort = value;
38
40
  if (protocol === "mysql") options.mysqlPort = value;
39
41
  if (protocol === "postgresql") options.pgsqlPort = value;
@@ -63,6 +65,10 @@ export function parseStartOptions(protocol: YydsProtocol, args: string[]): Start
63
65
  options.mysqlPort = Number(args[++index]);
64
66
  continue;
65
67
  }
68
+ if (argument === "--mongo-port") {
69
+ options.mongoPort = Number(args[++index]);
70
+ continue;
71
+ }
66
72
  if (argument === "--wire-port") {
67
73
  options.wirePort = Number(args[++index]);
68
74
  continue;
@@ -89,10 +95,27 @@ export async function runNodeStart(options: StartNodeOptions): Promise<void> {
89
95
  const identity = new native.NodeIdentityHandle(options.clusterId, options.nodeId);
90
96
  const lease = native.NodeLeaseHandle.acquire(options.dataDir, identity);
91
97
  const executor = new native.LocalExecutorHandle(options.dataDir);
92
- const redis = new native.RedisServiceHandle(lease, options.redisPort ?? DEFAULT_REDIS_PORT, executor);
93
- const pgsql = options.pgsqlPort === undefined ? undefined : new native.PgsqlServiceHandle(lease, options.pgsqlPort);
94
- const mysql = options.mysqlPort === undefined ? undefined : new native.MysqlServiceHandle(lease, options.mysqlPort);
95
- const wire = options.wirePort === undefined ? undefined : new native.WireServerHandle(options.wirePort, executor);
98
+ const redis = new native.RedisServiceHandle(
99
+ lease,
100
+ options.redisPort ?? DEFAULT_REDIS_PORT,
101
+ executor,
102
+ );
103
+ const pgsql =
104
+ options.pgsqlPort === undefined
105
+ ? undefined
106
+ : new native.PgsqlServiceHandle(lease, options.pgsqlPort);
107
+ const mysql =
108
+ options.mysqlPort === undefined
109
+ ? undefined
110
+ : new native.MysqlServiceHandle(lease, options.mysqlPort);
111
+ const mongo =
112
+ options.mongoPort === undefined
113
+ ? undefined
114
+ : new native.MongoServiceHandle(lease, options.mongoPort, executor);
115
+ const wire =
116
+ options.wirePort === undefined
117
+ ? undefined
118
+ : new native.WireServerHandle(options.wirePort, executor);
96
119
 
97
120
  const runtime = {
98
121
  version: 1,
@@ -107,7 +130,7 @@ export async function runNodeStart(options: StartNodeOptions): Promise<void> {
107
130
  native.nodeWriteRuntimeStatus(options.dataDir, JSON.stringify(runtime));
108
131
 
109
132
  process.stdout.write(
110
- `yyds running pid=${runtime.pid} redis=${runtime.redisAddress} pgsql=${runtime.pgsqlAddress ?? "null"} mysql=${runtime.mysqlAddress ?? "null"} wire=${runtime.wireAddress ?? "null"}\n`,
133
+ `yyds running pid=${runtime.pid} redis=${runtime.redisAddress} pgsql=${runtime.pgsqlAddress ?? "null"} mysql=${runtime.mysqlAddress ?? "null"} mongo=${mongo?.address() ?? "null"} wire=${runtime.wireAddress ?? "null"}\n`,
111
134
  );
112
135
 
113
136
  while (true) {
@@ -115,6 +138,7 @@ export async function runNodeStart(options: StartNodeOptions): Promise<void> {
115
138
  if (redis.isFinished()) break;
116
139
  if (pgsql?.isFinished()) break;
117
140
  if (mysql?.isFinished()) break;
141
+ if (mongo?.isFinished()) break;
118
142
  if (wire?.isFinished()) break;
119
143
  await sleep(50);
120
144
  }
@@ -122,6 +146,7 @@ export async function runNodeStart(options: StartNodeOptions): Promise<void> {
122
146
  redis.stop();
123
147
  pgsql?.stop();
124
148
  mysql?.stop();
149
+ mongo?.stop();
125
150
  wire?.stop();
126
151
  native.nodeClearRuntimeArtifacts(options.dataDir, runtime.generation);
127
152
  }
@@ -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
+ }