chore(services): update components based on recent changes
Changes: services/backend/src/ws/server.ts | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 72 insertions(+), 16 deletions(-)
This commit is contained in:
@@ -2,6 +2,7 @@ import type { Server } from "node:http";
|
|||||||
import { BACKEND_COMMAND, BACKEND_VOICE_TRANSMIT } from "@bete/shared";
|
import { BACKEND_COMMAND, BACKEND_VOICE_TRANSMIT } from "@bete/shared";
|
||||||
import { createChildLogger } from "@bete/shared/logger";
|
import { createChildLogger } from "@bete/shared/logger";
|
||||||
import { WebSocket, WebSocketServer } from "ws";
|
import { WebSocket, WebSocketServer } from "ws";
|
||||||
|
import { config } from "../shared/config/index.js";
|
||||||
import { setBroadcastFunctions } from "./broadcast.js";
|
import { setBroadcastFunctions } from "./broadcast.js";
|
||||||
|
|
||||||
const logger = createChildLogger("ws.server");
|
const logger = createChildLogger("ws.server");
|
||||||
@@ -63,21 +64,49 @@ export function closeWebSocketServer(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createWebSocketServer(server: Server): WebSocketServer {
|
export function createWebSocketServer(server: Server): WebSocketServer {
|
||||||
const clients = new Set<WebSocket>();
|
// Separate tracking: gateway sends PCM → forwarded to frontend only
|
||||||
|
const frontendClients = new Set<WebSocket>();
|
||||||
|
const gatewayClients = new Set<WebSocket>();
|
||||||
|
|
||||||
const wss = new WebSocketServer({ server, path: "/ws" });
|
const wss = new WebSocketServer({ server, path: "/ws" });
|
||||||
_wss = wss;
|
_wss = wss;
|
||||||
|
|
||||||
wss.on("connection", (ws: WebSocket) => {
|
wss.on("connection", (ws: WebSocket, req) => {
|
||||||
clients.add(ws);
|
// Parse auth token from query string
|
||||||
logger.info(`Client connected (${clients.size} total)`);
|
const rawUrl = req.url ?? "/";
|
||||||
|
let isGateway = false;
|
||||||
|
|
||||||
// Send initial states (user, ui, media) — fire-and-forget
|
try {
|
||||||
sendInitialStates(ws).catch((err) =>
|
const url = new URL(rawUrl, "http://localhost");
|
||||||
logger.error({ err }, "sendInitialStates failed"),
|
const token = url.searchParams.get("token");
|
||||||
);
|
isGateway =
|
||||||
|
token !== null &&
|
||||||
|
config.BACKEND_WS_TOKEN !== "" &&
|
||||||
|
token === config.BACKEND_WS_TOKEN;
|
||||||
|
} catch {
|
||||||
|
// Malformed URL — treat as frontend
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGateway) {
|
||||||
|
gatewayClients.add(ws);
|
||||||
|
logger.info("Discord gateway WebSocket client authenticated");
|
||||||
|
// Gateway doesn't need initial states
|
||||||
|
} else {
|
||||||
|
frontendClients.add(ws);
|
||||||
|
logger.info(`Frontend client connected (${frontendClients.size} total)`);
|
||||||
|
// Send initial states (user, ui, media) — fire-and-forget
|
||||||
|
sendInitialStates(ws).catch((err) =>
|
||||||
|
logger.error({ err }, "sendInitialStates failed"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ws.on("message", (data: Buffer) => {
|
ws.on("message", (data: Buffer) => {
|
||||||
|
// Gateway PCM forward — broadcast raw binary to frontend clients only
|
||||||
|
if (isGateway && Buffer.isBuffer(data)) {
|
||||||
|
broadcastBinaryToFrontend(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle binary PCM from browser (FE→Discord transmit)
|
// Handle binary PCM from browser (FE→Discord transmit)
|
||||||
// Format: 4-byte magic "PCM\0" + raw PCM Int16 LE
|
// Format: 4-byte magic "PCM\0" + raw PCM Int16 LE
|
||||||
if (
|
if (
|
||||||
@@ -173,20 +202,31 @@ export function createWebSocketServer(server: Server): WebSocketServer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ws.on("close", () => {
|
ws.on("close", () => {
|
||||||
clients.delete(ws);
|
if (isGateway) {
|
||||||
logger.info(`Client disconnected (${clients.size} total)`);
|
gatewayClients.delete(ws);
|
||||||
|
logger.info("Discord gateway WebSocket disconnected");
|
||||||
|
} else {
|
||||||
|
frontendClients.delete(ws);
|
||||||
|
logger.info(
|
||||||
|
`Frontend client disconnected (${frontendClients.size} total)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("error", (err: Error) => {
|
ws.on("error", (err: Error) => {
|
||||||
logger.error({ err }, "WebSocket client error");
|
logger.error({ err }, "WebSocket client error");
|
||||||
clients.delete(ws);
|
if (isGateway) {
|
||||||
|
gatewayClients.delete(ws);
|
||||||
|
} else {
|
||||||
|
frontendClients.delete(ws);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Heartbeat every 30s
|
// Heartbeat every 30s — frontend clients only
|
||||||
const heartbeatInterval = setInterval(() => {
|
const heartbeatInterval = setInterval(() => {
|
||||||
const message = JSON.stringify({ type: "heartbeat" });
|
const message = JSON.stringify({ type: "heartbeat" });
|
||||||
for (const client of clients) {
|
for (const client of frontendClients) {
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
client.send(message);
|
client.send(message);
|
||||||
}
|
}
|
||||||
@@ -196,13 +236,29 @@ export function createWebSocketServer(server: Server): WebSocketServer {
|
|||||||
// Don't let the interval keep the process alive after wss closes
|
// Don't let the interval keep the process alive after wss closes
|
||||||
heartbeatInterval.unref();
|
heartbeatInterval.unref();
|
||||||
|
|
||||||
// Defines broadcast functions and injects them via setBroadcastFunctions
|
// Forward gateway binary to frontend clients (no loopback to gateway)
|
||||||
|
function broadcastBinaryToFrontend(data: Buffer) {
|
||||||
|
for (const client of frontendClients) {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
try {
|
||||||
|
client.send(data);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
{ err },
|
||||||
|
"Failed to send binary to frontend client",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON event broadcast — frontend clients only
|
||||||
function broadcast(event: Omit<BroadcastEvent, "timestamp">) {
|
function broadcast(event: Omit<BroadcastEvent, "timestamp">) {
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
...event,
|
...event,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
for (const client of clients) {
|
for (const client of frontendClients) {
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
try {
|
try {
|
||||||
client.send(payload);
|
client.send(payload);
|
||||||
@@ -214,7 +270,7 @@ export function createWebSocketServer(server: Server): WebSocketServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function broadcastBinary(data: Buffer) {
|
function broadcastBinary(data: Buffer) {
|
||||||
for (const client of clients) {
|
for (const client of frontendClients) {
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
try {
|
try {
|
||||||
client.send(data);
|
client.send(data);
|
||||||
|
|||||||
Reference in New Issue
Block a user