Files
GMW/services/backend/src/orpc/ws.ts
T

44 lines
1.8 KiB
TypeScript
Raw Normal View History

import type { IncomingMessage, Server } from "node:http";
import type { Duplex } from "node:stream";
import { onError } from "@orpc/server";
import { RPCHandler } from "@orpc/server/ws";
import { WebSocketServer } from "ws";
import { createChildLogger } from "@/shared/logger/index";
import { appRouter } from "./router";
const logger = createChildLogger("orpc.ws");
/**
* Attach the oRPC WebSocket handler to the shared HTTP server, on a path
* SEPARATE from the voice/binary WebSocket (`/ws`). All structured data RPCs
* (dashboard, messages, moderation, media, voice control, recordings,
* analysis, chatbot, config, ui-state) flow over this `/trpc` socket; the
* `/ws` socket is left untouched for Discord PCM audio + gateway events.
*
* We use `noServer` + a manual `upgrade` router (instead of
* `new WebSocketServer({ server, path: "/trpc" })`) because two `ws` servers
* mounted with the `server` option on the SAME http.Server both register
* `upgrade` listeners, and `ws`'s path-guarded listener can reject (400) the
* other server's path. Routing the upgrade ourselves by URL keeps `/trpc`
* and `/ws` fully isolated.
*/
export function createORPCWebSocketServer(server: Server): WebSocketServer {
const handler = new RPCHandler(appRouter, {
interceptors: [
onError((error) => logger.error({ error }, "oRPC WS error")),
],
});
const wss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
server.on("upgrade", (req: IncomingMessage, socket: Duplex, head: Buffer) => {
if (!req.url?.startsWith("/trpc")) return; // let the /ws server handle it
wss.handleUpgrade(req, socket, head, (ws) => {
handler.upgrade(ws, { context: {} });
});
});
logger.info({ path: "/trpc" }, "oRPC WebSocket server attached");
return wss;
}