import { DurableObject } from "cloudflare:workers";
const MAX_SOCKETS = 200;
type Presence = { name: string };
/** One instance per project: fans out change notifications and presence to connected board viewers. */
export class ProjectRoom extends DurableObject<Env> {
async fetch(request: Request): Promise<Response> {
if (request.headers.get("upgrade") !== "websocket") return new Response("Expected websocket", { status: 426 });
if (this.ctx.getWebSockets().length >= MAX_SOCKETS) return new Response("Room is full", { status: 503 });
const name = request.headers.get("x-stig-viewer") ?? "anonymous";
const pair = new WebSocketPair();
this.ctx.acceptWebSocket(pair[1]);
pair[1].serializeAttachment({ name } satisfies Presence);
this.broadcastPresence();
return new Response(null, { status: 101, webSocket: pair[0] });
}
/** Called by server functions after a project mutation so open boards refresh. */
async notifyChanged(actor: string): Promise<void> {
this.send(JSON.stringify({ type: "changed", actor }));
}
async webSocketMessage(socket: WebSocket, message: string | ArrayBuffer): Promise<void> {
if (message === "ping") socket.send("pong");
}
async webSocketClose(socket: WebSocket): Promise<void> {
socket.close();
this.broadcastPresence();
}
private send(payload: string): void {
for (const socket of this.ctx.getWebSockets()) {
try {
socket.send(payload);
} catch {
socket.close();
}