import express from 'express'; import { createServer } from 'http'; import { Server } from 'socket.io'; import { PrismaClient } from '@prisma/client'; import pino from 'pino'; import { handler } from './build/handler.js'; const logger = pino({ name: 'pounce' }); const app = express(); const server = createServer(app); const io = new Server(server, { cors: { origin: process.env.ORIGIN || 'http://localhost:3000', credentials: true } }); // ─── Prisma client for session validation ─────────────────── const prisma = new PrismaClient(); // ─── Presence tracking ────────────────────────────────────── // Map> const presence = new Map(); function getPresenceList(boardId) { const room = presence.get(boardId); if (!room) return []; return Array.from(room.values()); } // ─── Cookie parser ────────────────────────────────────────── function parseCookies(cookieHeader) { const cookies = {}; if (!cookieHeader) return cookies; cookieHeader.split(';').forEach((cookie) => { const [name, ...rest] = cookie.trim().split('='); cookies[name] = decodeURIComponent(rest.join('=')); }); return cookies; } // ─── Socket.IO auth middleware ─────────────────────────────── io.use(async (socket, next) => { try { const cookies = parseCookies(socket.handshake.headers.cookie); const sessionId = cookies['session']; if (!sessionId) { socket.data.user = null; return next(); } const session = await prisma.session.findUnique({ where: { id: sessionId }, include: { user: { select: { id: true, name: true, avatarUrl: true, tenantId: true } } } }); if (session && session.expiresAt > new Date()) { socket.data.user = session.user; } else { socket.data.user = null; } next(); } catch (err) { logger.error({ err }, 'Socket auth error'); socket.data.user = null; next(); } }); // ─── Socket.IO connection handler ──────────────────────────── io.on('connection', (socket) => { const user = socket.data.user; logger.info({ socketId: socket.id, user: user?.name ?? 'anon' }, 'Socket connected'); // Join user-specific room for notifications if (user) { socket.join('user:' + user.id); } // Track which boards this socket has joined (for cleanup on disconnect) const joinedBoards = new Set(); socket.on('join-board', (boardId) => { socket.join(`board:${boardId}`); joinedBoards.add(boardId); if (user) { if (!presence.has(boardId)) presence.set(boardId, new Map()); presence.get(boardId).set(socket.id, { id: user.id, name: user.name, avatarUrl: user.avatarUrl }); } // Notify others socket.to(`board:${boardId}`).emit('user-joined', { user: user ? { id: user.id, name: user.name, avatarUrl: user.avatarUrl } : null, socketId: socket.id }); // Send full presence list to everyone in the room (including the joiner) io.to(`board:${boardId}`).emit('presence:update', getPresenceList(boardId)); }); socket.on('leave-board', (boardId) => { socket.leave(`board:${boardId}`); joinedBoards.delete(boardId); if (presence.has(boardId)) { presence.get(boardId).delete(socket.id); if (presence.get(boardId).size === 0) presence.delete(boardId); } socket.to(`board:${boardId}`).emit('user-left', { user: user ? { id: user.id, name: user.name } : null, socketId: socket.id }); // Update presence for remaining const room = presence.get(boardId); io.to(`board:${boardId}`).emit('presence:update', room ? Array.from(room.values()) : []); }); socket.on('disconnect', () => { logger.info({ socketId: socket.id }, 'Socket disconnected'); for (const boardId of joinedBoards) { if (presence.has(boardId)) { presence.get(boardId).delete(socket.id); if (presence.get(boardId).size === 0) { presence.delete(boardId); } } socket.to(`board:${boardId}`).emit('user-left', { user: user ? { id: user.id, name: user.name } : null, socketId: socket.id }); const room = presence.get(boardId); io.to(`board:${boardId}`).emit('presence:update', room ? Array.from(room.values()) : []); } }); }); // Store io globally so SvelteKit API routes can broadcast via realtime.ts globalThis.__socketIO = io; // SvelteKit handler app.use(handler); const port = process.env.PORT || 3000; const host = process.env.HOST || '0.0.0.0'; server.listen(port, host, () => { logger.info({ host, port }, 'Server running'); });