143 lines
4.8 KiB
TypeScript
143 lines
4.8 KiB
TypeScript
import { createServer, type Server as HttpServer } from "node:http";
|
|
import { type WebSocket, WebSocketServer } from "ws";
|
|
import { handleHealth } from "../../src/health/health.js";
|
|
import type {
|
|
Handshake,
|
|
WsServerFactory,
|
|
WsServerLike,
|
|
WsServerStartOptions,
|
|
WsServerStartResult,
|
|
WsSocketLike,
|
|
} from "../../src/types/types.js";
|
|
|
|
/**
|
|
* Production-grade `WsServerLike` implementation backed by the real
|
|
* `ws` package and a Node `http.Server`. The HTTP server also routes
|
|
* `/health` / `/ready` / `/live` probes through the daemon's snapshot
|
|
* so the integration test can hit those endpoints from the same port.
|
|
*/
|
|
export class RealWsServer implements WsServerLike {
|
|
#http: HttpServer | undefined;
|
|
#wss: WebSocketServer | undefined;
|
|
readonly #sockets = new Map<
|
|
string,
|
|
{ socket: WebSocket; like: WsSocketLike }
|
|
>();
|
|
readonly #snapshot: () => {
|
|
running: boolean;
|
|
disposed: boolean;
|
|
clientCount: number;
|
|
roomCount: number;
|
|
startedAt: number | undefined;
|
|
};
|
|
#nextId = 0;
|
|
|
|
public constructor(
|
|
snapshot: () => {
|
|
running: boolean;
|
|
disposed: boolean;
|
|
clientCount: number;
|
|
roomCount: number;
|
|
startedAt: number | undefined;
|
|
}
|
|
) {
|
|
this.#snapshot = snapshot;
|
|
}
|
|
|
|
public async start(
|
|
opts: WsServerStartOptions
|
|
): Promise<WsServerStartResult> {
|
|
const http = createServer((req, res) => {
|
|
const handled = handleHealth(req.url, this.#snapshot());
|
|
if (handled) {
|
|
res.writeHead(handled.status, {
|
|
"content-type": "application/json",
|
|
});
|
|
res.end(handled.body);
|
|
return;
|
|
}
|
|
res.writeHead(404, { "content-type": "text/plain" });
|
|
res.end("not found");
|
|
});
|
|
const wss = new WebSocketServer({ server: http });
|
|
wss.on("connection", (socket, request) => {
|
|
const id = `wsc-${++this.#nextId}`;
|
|
const url = request.url ?? "/";
|
|
const q = new URL(url, "http://x").searchParams;
|
|
const handshake: Handshake = {
|
|
headers: Object.fromEntries(
|
|
Object.entries(request.headers).map(([k, v]) => [
|
|
k,
|
|
Array.isArray(v) ? v.join(",") : v,
|
|
])
|
|
),
|
|
remoteAddress: request.socket.remoteAddress,
|
|
url,
|
|
query: Object.fromEntries(q.entries()),
|
|
};
|
|
const like: WsSocketLike = {
|
|
id,
|
|
remoteAddress: request.socket.remoteAddress,
|
|
send: (payload) => {
|
|
if (socket.readyState === socket.OPEN) socket.send(payload);
|
|
},
|
|
close: (code, reason) => socket.close(code, reason),
|
|
};
|
|
this.#sockets.set(id, { socket, like });
|
|
opts.callbacks.onConnection(like, handshake);
|
|
socket.on("message", (data) =>
|
|
opts.callbacks.onMessage(id, data.toString())
|
|
);
|
|
socket.on("close", (code, reason) => {
|
|
this.#sockets.delete(id);
|
|
opts.callbacks.onClose(id, code, reason.toString());
|
|
});
|
|
socket.on("error", (err) => opts.callbacks.onError(id, err));
|
|
});
|
|
await new Promise<void>((resolve, reject) => {
|
|
const onErr = (err: Error): void => reject(err);
|
|
http.once("error", onErr);
|
|
http.listen(opts.port, opts.host, () => {
|
|
http.removeListener("error", onErr);
|
|
resolve();
|
|
});
|
|
});
|
|
this.#http = http;
|
|
this.#wss = wss;
|
|
const addr = http.address() as { address: string; port: number };
|
|
return { address: addr.address, port: addr.port };
|
|
}
|
|
|
|
public async stop(): Promise<void> {
|
|
await new Promise<void>((resolve) => {
|
|
this.#wss?.close(() => resolve());
|
|
});
|
|
await new Promise<void>((resolve) => {
|
|
const http = this.#http;
|
|
if (!http) {
|
|
resolve();
|
|
return;
|
|
}
|
|
http.close(() => resolve());
|
|
});
|
|
}
|
|
|
|
public async closeAll(code = 1001, reason = "shutdown"): Promise<void> {
|
|
for (const { socket } of this.#sockets.values())
|
|
socket.close(code, reason);
|
|
}
|
|
}
|
|
|
|
/** Build a real-ws factory bound to a snapshot accessor. */
|
|
export function makeRealWsServerFactory(
|
|
snapshot: () => {
|
|
running: boolean;
|
|
disposed: boolean;
|
|
clientCount: number;
|
|
roomCount: number;
|
|
startedAt: number | undefined;
|
|
}
|
|
): WsServerFactory {
|
|
return () => new RealWsServer(snapshot);
|
|
}
|