import type { AddressInfo } from "node:net" import { networkInterfaces } from "node:os" import { WebSocketServer as Server } from "ws" const internalFormatWebSocketHost = (address: string): string => { const normalizedAddress = address.replaceAll("%", "%25") if (normalizedAddress.includes(":")) { return `[${normalizedAddress}]` } return normalizedAddress } const internalBuildWebSocketUrlList = (port: number): string[] => { const urlSet = new Set([ `ws://127.0.0.1:${port}`, `ws://localhost:${port}`, `ws://[::1]:${port}`, ]) for (const networkInterfaceInfo of Object.values(networkInterfaces()).flat()) { if ( networkInterfaceInfo === undefined || networkInterfaceInfo.address.length === 0 || networkInterfaceInfo.internal === true || networkInterfaceInfo.address === "0.0.0.0" || networkInterfaceInfo.address === "::" ) { continue } const address = internalFormatWebSocketHost(networkInterfaceInfo.address) urlSet.add(`ws://${address}:${port}`) } return [...urlSet] } const internalGetListeningPort = (server: Server): number => { const address = server.address() if (address === null) { throw new Error("Server is not running.") } if (typeof address === "string") { throw new TypeError("WebSocket server is not listening on a TCP port.") } return (address as AddressInfo).port } /** * @description 表示 WebSocketServer 启动后的结果。 */ export interface WebSocketServerStartResult { urlList: string[] } /** * @description 表示 WebSocketServer 的构造参数。 */ export interface WebSocketServerOptions { port: number } /** * @description 对原生 WebSocketServer 进行包装,并补齐更稳定的运行时语义。 */ export class WebSocketServer { protected readonly options: WebSocketServerOptions protected server: Server | undefined protected startResult: WebSocketServerStartResult | undefined constructor(options: WebSocketServerOptions) { this.options = options this.server = undefined this.startResult = undefined } /** * @description 返回当前运行中的原生 WebSocketServer 实例。 */ getServer(): Server { if (this.server === undefined) { throw new Error("Server is not running.") } if (this.server.address() === null) { this.server = undefined throw new Error("Server is not running.") } return this.server } getStartResult(): WebSocketServerStartResult { if (this.startResult === undefined) { throw new Error("Server is not running.") } return structuredClone(this.startResult) } protected setRunningServer(server: Server, startResult: WebSocketServerStartResult): void { this.server = server this.startResult = structuredClone(startResult) } protected clearRunningServer(server?: Server): void { if (server !== undefined && this.server !== undefined && this.server !== server) { return } this.server = undefined this.startResult = undefined } /** * @description 返回服务当前是否处于可接受连接的运行状态。 */ isRunning(): boolean { if (this.server === undefined) { return false } if (this.server.address() === null) { this.clearRunningServer() return false } return true } /** * @description 启动服务并返回可直接使用的本地地址列表。 */ async start(): Promise { if (this.isRunning() === true) { throw new Error("Server is already running.") } const server = new Server({ port: this.options.port }) const result = await new Promise((resolve, reject) => { const internalHandleError = (error: Error): void => { server.off("listening", internalHandleListening) this.clearRunningServer() reject(error) } const internalHandleListening = (): void => { server.off("error", internalHandleError) this.setRunningServer(server, { urlList: [] }) server.once("close", () => { if (this.server === server) { this.clearRunningServer() } }) const port = internalGetListeningPort(server) const urlList = internalBuildWebSocketUrlList(port) this.setRunningServer(server, { urlList }) resolve({ urlList }) } server.once("error", internalHandleError) server.once("listening", internalHandleListening) }) return result } /** * @description 关闭当前服务实例。 */ async close(): Promise { if (this.isRunning() === false) { return } const server = this.getServer() await new Promise((resolve, reject) => { server.close((error) => { if (error !== undefined) { reject(error) return } resolve() }) }) } } /** * @description 创建一个 WebSocketServer 包装实例。 */ export const createWebSocketServer = (options: WebSocketServerOptions): WebSocketServer => { return new WebSocketServer(options) }