import type { Server } from "bun";
import {
broadcastToChannel,
isInChannel,
joinChannel,
leaveAllChannels,
} from "./channels";
import type { ChannelWebSocket, IncomingMessage, WebSocketData } from "./types";
/**
* Handle new WebSocket connection
*/
function handleConnection(ws: ChannelWebSocket): void {
console.log("[RELAY] New client connected");
// Send welcome message
ws.send(
JSON.stringify({
type: "system",
message: "Connected to relay server. Please join a channel to start.",
}),
);
}
/**
* Handle WebSocket message
*/
function handleMessage(ws: ChannelWebSocket, message: string | Buffer): void {
try {
const data = JSON.parse(message.toString()) as IncomingMessage;
console.log(
`\n[RELAY] Received: type=${data.type}, channel=${data.channel || "N/A"}`,
);
if (data.message?.command) {
console.log(`[RELAY] Command: ${data.message.command}, id=${data.id}`);
} else if (data.message?.result !== undefined) {
console.log(
`[RELAY] Response: id=${data.id}, hasResult=${data.message.result !== undefined}`,
);
}
// Handle join request
if (data.type === "join") {
handleJoin(ws, data);
return;
}
// Handle regular messages
if (data.type === "message") {
handleChannelMessage(ws, data);
return;
}
console.log(`[RELAY] Unknown message type: ${data.type}`);
} catch (error) {
console.error("[RELAY] Error parsing message:", error);
ws.send(
JSON.stringify({
type: "error",
message: "Invalid message format",
}),
);
}
}
/**
* Handle channel join request
*/
function handleJoin(ws: ChannelWebSocket, data: IncomingMessage): void {
const channelName = data.channel;
if (!channelName || typeof channelName !== "string") {
ws.send(
JSON.stringify({
type: "error",
message: "Channel name is required",
}),
);
return;
}
// Add client to channel
const clientCount = joinChannel(ws, channelName);
ws.data.channel = channelName;
console.log(
`[RELAY] Client joined channel "${channelName}" (${clientCount} total clients)`,
);
// Notify client they joined successfully
ws.send(
JSON.stringify({
type: "system",
message: `Joined channel: ${channelName}`,
channel: channelName,
}),
);
// Send join confirmation with ID for pending request resolution
ws.send(
JSON.stringify({
type: "system",
channel: channelName,
message: {
id: data.id,
result: `Connected to channel: ${channelName}`,
},
}),
);
// Notify other clients in channel
broadcastToChannel(
ws,
channelName,
JSON.stringify({
type: "system",
message: "A new client has joined the channel",
channel: channelName,
}),
);
}
/**
* Handle channel message (relay to other clients)
*/
function handleChannelMessage(
ws: ChannelWebSocket,
data: IncomingMessage,
): void {
const channelName = data.channel;
if (!channelName || typeof channelName !== "string") {
ws.send(
JSON.stringify({
type: "error",
message: "Channel name is required",
}),
);
return;
}
if (!isInChannel(ws, channelName)) {
ws.send(
JSON.stringify({
type: "error",
message: "You must join the channel first",
}),
);
return;
}
// Broadcast to all other clients in the channel
const broadcastMessage = JSON.stringify({
type: "broadcast",
message: data.message,
sender: "peer",
channel: channelName,
});
const count = broadcastToChannel(ws, channelName, broadcastMessage);
if (count === 0) {
console.log(
`[RELAY] No other clients in channel "${channelName}" to receive message`,
);
} else {
console.log(
`[RELAY] Broadcast to ${count} peer(s) in channel "${channelName}"`,
);
}
}
/**
* Handle WebSocket close
*/
function handleClose(ws: ChannelWebSocket): void {
console.log("[RELAY] Client disconnected");
leaveAllChannels(ws);
}
/**
* Create and start the WebSocket relay server
*/
export function createRelayServer(port: number = 3055): Server<WebSocketData> {
const server = Bun.serve<WebSocketData>({
port,
// Uncomment to allow connections from Windows WSL
// hostname: "0.0.0.0",
fetch(req: Request, server: Server<WebSocketData>) {
// Handle CORS preflight
if (req.method === "OPTIONS") {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
// Upgrade to WebSocket
const success = server.upgrade(req, {
data: { channel: undefined },
headers: {
"Access-Control-Allow-Origin": "*",
},
});
if (success) {
return; // Upgraded to WebSocket
}
// Return response for non-WebSocket requests
return new Response("WebSocket relay server running", {
headers: {
"Access-Control-Allow-Origin": "*",
},
});
},
websocket: {
open: handleConnection,
message: handleMessage,
close: handleClose,
},
});
return server;
}