Files
homarr/apps/nextjs/server.ts
T
copilot-swe-agent[bot]andajnart c8ae7f283a feat: implement custom Next.js server with integrated WebSocket (POC)
- Create apps/nextjs/server.ts custom server integrating WebSocket via ws
- Update nextjs scripts to use custom server for dev and start
- Update root start script to remove separate ws server process
- Update client tRPC URL to connect to port 3000 in dev
- Update nginx.conf to route /websockets to port 3000
- Update Dockerfile and run.sh for integrated server
- Disable standalone ws server dev script
- Add benchmark-memory.sh script

Co-authored-by: ajnart <49837342+ajnart@users.noreply.github.com>
2026-02-28 08:58:34 +00:00

94 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createServer } from "http";
import next from "next";
import { parse } from "url";
import { applyWSSHandler } from "@trpc/server/adapters/ws";
import { WebSocketServer } from "ws";
import { appRouter, createTRPCContext } from "@homarr/api/websocket";
import { getSessionFromToken, sessionTokenCookieName } from "@homarr/auth";
import { parseCookies } from "@homarr/common";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { db } from "@homarr/db";
const logger = createLogger({ module: "customServer" });
const dev = process.env.NODE_ENV !== "production";
const hostname = process.env.HOSTNAME || "localhost";
const port = parseInt(process.env.PORT || "3000", 10);
const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = createServer((req, res) => {
const parsedUrl = parse(req.url!, true);
handle(req, res, parsedUrl);
});
// Create WebSocket server in noServer mode so we handle upgrade manually
const wss = new WebSocketServer({ noServer: true });
const wssHandler = applyWSSHandler({
wss,
router: appRouter,
// eslint-disable-next-line no-restricted-syntax
createContext: async ({ req }) => {
try {
const headers = Object.entries(req.headers).map(
([key, value]) => [key, typeof value === "string" ? value : value?.[0]] as [string, string],
);
const nextHeaders = new Headers(headers);
const store = parseCookies(nextHeaders.get("cookie") ?? "");
const sessionToken = store[sessionTokenCookieName];
const session = await getSessionFromToken(db, sessionToken);
return createTRPCContext({
headers: nextHeaders,
session,
});
} catch (error) {
logger.error(error);
return createTRPCContext({
headers: new Headers(),
session: null,
});
}
},
keepAlive: {
enabled: true,
pingMs: 30000,
pongWaitMs: 5000,
},
});
// Handle WebSocket upgrade requests on /websockets path
server.on("upgrade", (req, socket, head) => {
if (req.url?.startsWith("/websockets")) {
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
});
}
});
wss.on("connection", (websocket, incomingMessage) => {
logger.info(`➕ Connection (${wss.clients.size}) ${incomingMessage.method} ${incomingMessage.url}`);
websocket.once("close", (code, reason) => {
logger.info(`➖ Connection (${wss.clients.size}) ${code} ${reason.toString()}`);
});
});
server.listen(port, () => {
logger.info(`✅ Custom server ready on http://${hostname}:${port}`);
logger.info(`✅ WebSocket Server integrated on ws://${hostname}:${port}/websockets`);
});
process.on("SIGTERM", () => {
logger.info("SIGTERM");
wssHandler.broadcastReconnectNotification();
wss.close();
server.close();
});
});