Working ish architecture

This commit is contained in:
2026-02-09 11:26:12 +01:00
parent 04bb5d1b08
commit 25fd5f207e
7 changed files with 123 additions and 108 deletions
+38 -84
View File
@@ -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<ArrayBuffer>) => void;
post?: (req: Request) => void;
}
/**
* One run = many websocket clients
*/
type LiveRun = {
clients: Set<WebSocket>;
};
export class NextWebsocketApp {
handlers: Record<string, WsRouteHandler> = {};
const runs: Record<string, LiveRun> = {};
on<K extends keyof WsRouteHandler>(
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<ArrayBuffer>) => 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<K extends keyof WsRouteHandler>(
method: K,
route: string,
...args: Parameters<NonNullable<WsRouteHandler[K]>>
) {
const handler = this.handlers[route]?.[method] as
| ((...args: Parameters<NonNullable<WsRouteHandler[K]>>) => 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);
}
}