44 lines
1.8 KiB
TypeScript
44 lines
1.8 KiB
TypeScript
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;
|
|
}
|