Co-authored-by: ajnart <49837342+ajnart@users.noreply.github.com>
100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
// Must be imported first to set up globalThis.AsyncLocalStorage for Next.js
|
||
import "next/dist/server/node-environment-baseline";
|
||
|
||
import { createServer } from "http";
|
||
import path from "path";
|
||
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);
|
||
// Resolve the directory of this file so Next.js can find the app/pages dirs
|
||
const dir = path.resolve(__dirname);
|
||
|
||
const app = next({ dev, hostname, port, dir });
|
||
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();
|
||
});
|
||
});
|