From 25fd5f207ef45e1ca6cae90adab424fc28e22fc4 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Mon, 9 Feb 2026 11:26:12 +0100 Subject: [PATCH] Working ish architecture --- package.json | 4 +- src/bin/index.ts | 76 ++++++++++++++++++++++++++++ src/build.ts | 0 src/index.ts | 122 ++++++++++++++------------------------------- src/lib/next-ws.ts | 0 src/read.ts | 22 -------- ws.config.ts | 7 +++ 7 files changed, 123 insertions(+), 108 deletions(-) create mode 100644 src/bin/index.ts delete mode 100644 src/build.ts delete mode 100644 src/lib/next-ws.ts delete mode 100644 src/read.ts create mode 100644 ws.config.ts diff --git a/package.json b/package.json index 073dae2..ca93a8f 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,11 @@ "license": "Apache-2.0", "type": "module", "bin": { - "next-websockets": "dist/index.js" + "next-websockets": "dist/bin/index.js" }, "scripts": { "build": "tsc", - "dev": "tsc && node dist/index.js" + "dev": "tsc && node dist/bin/index.js" }, "dependencies": { "@types/cli-color": "^2.0.6", diff --git a/src/bin/index.ts b/src/bin/index.ts new file mode 100644 index 0000000..7fa4cc1 --- /dev/null +++ b/src/bin/index.ts @@ -0,0 +1,76 @@ +#!/usr/bin/env node + +import clc from "cli-color"; +import { createServer, IncomingMessage } from "http"; +import next from "next"; +import path from "path"; +import { pathToFileURL } from "url"; +import { WebSocketServer, WebSocket } from "ws"; +import { NextWebsocketConfig } from ".."; +import { Readable } from "stream"; + +const dev = process.env.NODE_ENV !== "production"; +const app = next({ dev }); +const handle = app.getRequestHandler(); + +const config = await loadOptionalConfig(); + +app.prepare().then(() => { + const server = createServer((req, res) => { + handle(req, res); + }); + + const wss = new WebSocketServer({ noServer: true }); + + // WebSocket connections + wss.on("connection", (ws: WebSocket, req: IncomingMessage) => {}); + + // Upgrade HTTP → WebSocket + server.on("upgrade", (req, socket, head) => { + if (!req.url) return; + + config?.app.emit("upgrade", req.url, toRequest(req), socket, head); + if (req.url === "/api/view-run") { + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit("connection", ws, req); + }); + } + }); + + server.listen(3000, () => { + console.log(clc.red(" ▲ Next.js (websockets)")); + console.log(` - Local: http://localhost:3000`); + console.log(` - Network: http://192.168.100.3:3000`); + console.log(` - Environments: .env`); + }); +}); + +export async function loadOptionalConfig( + filename = "ws.config.ts", +): Promise { + const absPath = path.resolve(process.cwd(), filename); + + try { + const mod = await import(pathToFileURL(absPath).href); + return mod.default ?? mod; + } catch { + return undefined; + } +} + +function toRequest(req: IncomingMessage): Request { + const host = req.headers.host ?? "localhost"; + + const url = new URL(req.url ?? "/", `http://${host}`); + + const body = + req.method !== "GET" && req.method !== "HEAD" + ? new ReadableStream(Readable.toWeb(req)) + : undefined; + + return new Request(url.toString(), { + method: req.method, + headers: req.headers as HeadersInit, + body, + }); +} diff --git a/src/build.ts b/src/build.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/index.ts b/src/index.ts index 90966bf..840aedd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,92 +1,46 @@ -#!/usr/bin/env node +import { Duplex } from "stream"; -import clc from "cli-color"; -import { createServer, IncomingMessage } from "http"; -import next from "next"; -import { WebSocketServer, WebSocket } from "ws"; +export interface NextWebsocketConfig { + app: NextWebsocketApp; +} -const dev = process.env.NODE_ENV !== "production"; -const app = next({ dev }); -const handle = app.getRequestHandler(); +export interface WsRouteHandler { + upgrade?: (req: Request, socket: Duplex, head: Buffer) => void; + post?: (req: Request) => void; +} -/** - * One run = many websocket clients - */ -type LiveRun = { - clients: Set; -}; +export class NextWebsocketApp { + handlers: Record = {}; -const runs: Record = {}; + on( + method: K, + route: string, + handler: WsRouteHandler[K], + ) { + if (!this.handlers[route]) this.handlers[route] = {}; + this.handlers[route][method] = handler; + } -app.prepare().then(() => { - const server = createServer((req, res) => { - handle(req, res); - }); + upgrade( + route: string, + handler: (req: Request, socket: Duplex, head: Buffer) => void, + ) { + this.on("upgrade", route, handler); + } - const wss = new WebSocketServer({ noServer: true }); + post(route: string, handler: () => void) { + this.on("post", route, handler); + } - // WebSocket connections - wss.on("connection", (ws: WebSocket, req: IncomingMessage) => { - ws.on("message", (msg: Buffer) => { - const runId = msg.toString(); + emit( + method: K, + route: string, + ...args: Parameters> + ) { + const handler = this.handlers[route]?.[method] as + | ((...args: Parameters>) => unknown) + | undefined; - if (!runs[runId]) { - runs[runId] = { clients: new Set() }; - } - - runs[runId].clients.add(ws); - - ws.once("close", () => { - runs[runId]?.clients.delete(ws); - }); - }); - }); - - // Handle POST requests - server.on("request", (req, res) => { - if (req.method === "POST" && req.url === "/api/run/move") { - let body = ""; - - req.on("data", (chunk: Buffer) => { - body += chunk.toString(); - }); - - req.on("end", () => { - try { - const move: { - runId: string; - moves: unknown; - } = JSON.parse(body); - - runs[move.runId]?.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(JSON.stringify(move.moves)); - } - }); - - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ status: "ok" })); - } catch (err) { - res.writeHead(400); - res.end("Invalid JSON"); - } - }); - } - }); - - // Upgrade HTTP → WebSocket - server.on("upgrade", (req, socket, head) => { - if (req.url === "/api/view-run") { - wss.handleUpgrade(req, socket, head, (ws) => { - wss.emit("connection", ws, req); - }); - } - }); - - server.listen(3000, () => { - console.log(clc.red(" ▲ Next.js (websockets)")); - console.log(` - Local: http://localhost:3000`); - console.log(` - Network: http://192.168.100.3:3000`); - console.log(` - Environments: .env`); - }); -}); + handler?.(...args); + } +} diff --git a/src/lib/next-ws.ts b/src/lib/next-ws.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/read.ts b/src/read.ts deleted file mode 100644 index e841d1e..0000000 --- a/src/read.ts +++ /dev/null @@ -1,22 +0,0 @@ -import fs from "fs"; -import path from "path"; - -export function listWsFiles(dir: string) { - const files: string[] = []; - - function walk(currentDir: string) { - const entries = fs.readdirSync(currentDir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(currentDir, entry.name); - if (entry.isDirectory()) { - walk(fullPath); - } else if (entry.isFile() && entry.name === "ws.ts") { - files.push(fullPath); - } - } - } - - walk(dir); - - return files; -} diff --git a/ws.config.ts b/ws.config.ts new file mode 100644 index 0000000..5535e7e --- /dev/null +++ b/ws.config.ts @@ -0,0 +1,7 @@ +import { NextWebsocketApp, NextWebsocketConfig } from "./src/index"; + +const app = new NextWebsocketApp(); + +export default { + app, +} satisfies NextWebsocketConfig;