Working ish architecture
This commit is contained in:
+2
-2
@@ -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",
|
||||
|
||||
@@ -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<NextWebsocketConfig | undefined> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
+35
-81
@@ -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> = {};
|
||||
|
||||
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) => {
|
||||
ws.on("message", (msg: Buffer) => {
|
||||
const runId = msg.toString();
|
||||
|
||||
if (!runs[runId]) {
|
||||
runs[runId] = { clients: new Set() };
|
||||
on<K extends keyof WsRouteHandler>(
|
||||
method: K,
|
||||
route: string,
|
||||
handler: WsRouteHandler[K],
|
||||
) {
|
||||
if (!this.handlers[route]) this.handlers[route] = {};
|
||||
this.handlers[route][method] = handler;
|
||||
}
|
||||
|
||||
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));
|
||||
upgrade(
|
||||
route: string,
|
||||
handler: (req: Request, socket: Duplex, head: Buffer<ArrayBuffer>) => void,
|
||||
) {
|
||||
this.on("upgrade", route, handler);
|
||||
}
|
||||
});
|
||||
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ status: "ok" }));
|
||||
} catch (err) {
|
||||
res.writeHead(400);
|
||||
res.end("Invalid JSON");
|
||||
post(route: string, handler: () => void) {
|
||||
this.on("post", route, handler);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
-22
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { NextWebsocketApp, NextWebsocketConfig } from "./src/index";
|
||||
|
||||
const app = new NextWebsocketApp();
|
||||
|
||||
export default {
|
||||
app,
|
||||
} satisfies NextWebsocketConfig;
|
||||
Reference in New Issue
Block a user