From 2f36d931fd8972487ada7959f4b91dcd9c66b5da Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Thu, 16 Oct 2025 17:19:32 -0700 Subject: [PATCH] feat: plan Socket.IO server setup and client hook for real-time draft updates --- plans/draft-room-implementation.md | 331 +++++++++++++++++- plans/phase-3-server-js-changes.md | 130 +++++++ plans/phase-3-socket-implementation-plan.md | 278 +++++++++++++++ .../phase-3-socket-implementation-summary.md | 111 ++++++ 4 files changed, 834 insertions(+), 16 deletions(-) create mode 100644 plans/phase-3-server-js-changes.md create mode 100644 plans/phase-3-socket-implementation-plan.md create mode 100644 plans/phase-3-socket-implementation-summary.md diff --git a/plans/draft-room-implementation.md b/plans/draft-room-implementation.md index 0cd2f45..c5f8727 100644 --- a/plans/draft-room-implementation.md +++ b/plans/draft-room-implementation.md @@ -90,27 +90,165 @@ Build a real-time draft room using Socket.IO for live updates, with snake draft ## Phase 3: Socket.IO Setup +### Overview + +Socket.IO requires access to the HTTP server instance (not just the Express app). The key pattern is: +```javascript +const server = http.createServer(app); +const io = socketIo(server); +server.listen(port); // NOT app.listen() +``` + +### Architecture Decision + +**Option A: Modify server.js (Recommended)** +- Change `app.listen()` to `http.createServer(app)` + `server.listen()` +- Initialize Socket.IO with the HTTP server +- Keep everything in one process +- ✅ Simple, standard pattern +- ✅ Socket.IO and Express share same port +- ✅ Works with React Router v7 SSR +- ❌ Requires modifying server.js + +**Option B: Separate Socket Server** +- Run Socket.IO on different port (e.g., 3001) +- Keep server.js untouched +- ✅ No changes to existing server +- ❌ CORS complexity +- ❌ Two processes to manage +- ❌ Different ports for HTTP and WebSocket + +**Decision: Use Option A** - Industry standard, simpler deployment, better DX + ### 3.1 Server Setup -- Install `socket.io` -- Create `server/socket.ts` -- Initialize Socket.IO server alongside Remix -- Set up draft room namespaces: `draft-${seasonId}` +#### Step 1: Modify server.js to use HTTP server + +**File: `server.js`** + +Change from: +```javascript +app.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); +}); +``` + +To: +```javascript +import { createServer } from "http"; + +const httpServer = createServer(app); + +// Import and initialize Socket.IO +const { initializeSocketIO } = await import("./server/socket.js"); +initializeSocketIO(httpServer); + +httpServer.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); +}); +``` + +#### Step 2: Create Socket.IO module + +**File: `server/socket.js`** (use .js not .ts to avoid bundling issues) + +```javascript +import { Server as SocketIOServer } from "socket.io"; + +let io; + +export function initializeSocketIO(httpServer) { + if (io) { + console.log("Socket.IO already initialized"); + return io; + } + + io = new SocketIOServer(httpServer, { + cors: { + origin: process.env.NODE_ENV === "production" + ? process.env.APP_URL + : `http://localhost:${process.env.PORT || 3000}`, + credentials: true, + }, + }); + + io.on("connection", (socket) => { + console.log("Client connected:", socket.id); + + // Join draft room + socket.on("join-draft", (seasonId) => { + socket.join(`draft-${seasonId}`); + console.log(`Socket ${socket.id} joined draft-${seasonId}`); + }); + + // Leave draft room + socket.on("leave-draft", (seasonId) => { + socket.leave(`draft-${seasonId}`); + console.log(`Socket ${socket.id} left draft-${seasonId}`); + }); + + socket.on("disconnect", () => { + console.log("Client disconnected:", socket.id); + }); + }); + + // Store globally for access in route handlers + global.__socketIO = io; + + console.log("Socket.IO initialized"); + return io; +} + +export function getSocketIO() { + if (!io && !global.__socketIO) { + throw new Error("Socket.IO not initialized"); + } + return io || global.__socketIO; +} +``` + +#### Step 3: Add global type definition + +**File: `server/types.d.ts`** + +```typescript +import type { Server as SocketIOServer } from "socket.io"; + +declare global { + var __socketIO: SocketIOServer | undefined; +} + +export {}; +``` + +#### Step 4: Update tsconfig.node.json + +Add server directory to includes: +```json +{ + "include": [ + "server.js", + "server/**/*.js", + "server/**/*.ts", + "vite.config.ts" + ] +} +``` ### 3.2 Socket Events (Server) **Listen for:** - `join-draft` - user joins draft room -- `make-pick` - user makes a pick -- `add-to-queue` - add player to queue -- `remove-from-queue` - remove from queue -- `reorder-queue` - reorder queue -- `commissioner-force-pick` - commissioner forces pick -- `commissioner-start-draft` - start draft -- `commissioner-pause-draft` - pause/resume draft +- `leave-draft` - user leaves draft room +- `make-pick` - user makes a pick (handled via HTTP POST, not socket) +- `add-to-queue` - add player to queue (handled via HTTP POST) +- `remove-from-queue` - remove from queue (handled via HTTP POST) +- `reorder-queue` - reorder queue (handled via HTTP POST) -**Emit:** +**Note:** Write operations (picks, queue changes) should use HTTP POST requests for reliability and error handling. Socket.IO is for broadcasting updates to all clients. + +**Emit (from server to clients):** - `pick-made` - broadcast new pick to room - `timer-update` - broadcast timer updates (every second) @@ -122,10 +260,171 @@ Build a real-time draft room using Socket.IO for live updates, with snake draft ### 3.3 Client Hook -- Create `hooks/useDraftSocket.ts` -- Handle connection/disconnection -- Provide methods to emit events -- Provide listeners for events +**File: `app/hooks/useDraftSocket.ts`** + +```typescript +import { useEffect, useRef, useState } from "react"; +import { io, Socket } from "socket.io-client"; + +export function useDraftSocket(seasonId: string) { + const socketRef = useRef(null); + const [isConnected, setIsConnected] = useState(false); + + useEffect(() => { + // Connect to Socket.IO server + const socket = io({ + path: "/socket.io", + transports: ["websocket", "polling"], + }); + + socketRef.current = socket; + + socket.on("connect", () => { + console.log("Connected to Socket.IO"); + setIsConnected(true); + // Join the draft room + socket.emit("join-draft", seasonId); + }); + + socket.on("disconnect", () => { + console.log("Disconnected from Socket.IO"); + setIsConnected(false); + }); + + // Cleanup on unmount + return () => { + if (socket) { + socket.emit("leave-draft", seasonId); + socket.disconnect(); + } + }; + }, [seasonId]); + + // Helper to subscribe to events + const on = (event: string, callback: (...args: any[]) => void) => { + socketRef.current?.on(event, callback); + }; + + // Helper to unsubscribe from events + const off = (event: string, callback?: (...args: any[]) => void) => { + socketRef.current?.off(event, callback); + }; + + return { socket: socketRef.current, isConnected, on, off }; +} +``` + +### 3.4 Using Socket in Route Handlers + +When a pick is made via HTTP POST, broadcast to all clients: + +**Example in `app/routes/api/draft-pick.ts`:** + +```typescript +import { getSocketIO } from "~/server/socket.js"; + +export async function action({ request }: ActionFunctionArgs) { + // ... validate and create pick ... + + const pick = await createDraftPick({...}); + + // Broadcast to all clients in the draft room + const io = getSocketIO(); + io.to(`draft-${seasonId}`).emit("pick-made", { + pick, + nextPickNumber, + isDraftComplete, + }); + + return json({ success: true, pick }); +} +``` + +### 3.5 Testing Socket.IO + +1. Start dev server: `npm run dev` +2. Check console for "Socket.IO initialized" +3. Open browser console on draft page +4. Should see "Connected to Socket.IO" +5. Check server logs for "Client connected: [socket-id]" + +### 3.6 Gotchas & Important Notes + +#### ⚠️ Gotcha #1: Import path in route handlers +```typescript +// ❌ WRONG - This won't work in React Router v7 +import { getSocketIO } from "~/server/socket.js"; + +// ✅ CORRECT - Use relative path or configure alias +import { getSocketIO } from "../../../server/socket.js"; +``` +**Why:** React Router v7 runs server code, but `~` alias typically only works for app code. You may need to configure the import path or use relative imports. + +#### ⚠️ Gotcha #2: Client hook creates new connection on every render +The current `useDraftSocket` hook will reconnect if `seasonId` changes. This is correct, but be aware: +- Don't call this hook multiple times in the same component tree +- Don't pass a changing `seasonId` unless you want to switch rooms +- Consider memoizing the `seasonId` prop + +#### ⚠️ Gotcha #3: Event listener cleanup +```typescript +// ❌ WRONG - Listeners not cleaned up +useEffect(() => { + socket.on("pick-made", handlePickMade); +}, []); + +// ✅ CORRECT - Clean up listeners +useEffect(() => { + socket.on("pick-made", handlePickMade); + return () => { + socket.off("pick-made", handlePickMade); + }; +}, [handlePickMade]); +``` + +#### ⚠️ Gotcha #4: Socket.IO path in production +If you deploy behind a reverse proxy (nginx, etc.), you may need to configure the Socket.IO path: +```javascript +// Client +const socket = io({ + path: "/socket.io", // Make sure this matches your proxy config +}); + +// Server (if needed) +const io = new SocketIOServer(httpServer, { + path: "/socket.io", +}); +``` + +#### ⚠️ Gotcha #5: CORS might not be needed +Since Socket.IO runs on the same server/port as your app, CORS might be unnecessary for same-origin requests. You can simplify: +```javascript +// Simpler config if same-origin +const io = new SocketIOServer(httpServer); +``` + +#### ⚠️ Gotcha #6: Global storage in development +The global `__socketIO` pattern works, but in development with HMR, you might get multiple instances. The `if (io)` check helps, but be aware of potential stale connections during hot reloads. + +#### ⚠️ Gotcha #7: TypeScript import in route handlers +```typescript +// This might cause issues because socket.js is JavaScript +import { getSocketIO } from "~/server/socket.js"; + +// Consider making it TypeScript with proper types +// Or use @ts-ignore if necessary +// @ts-ignore +import { getSocketIO } from "~/server/socket.js"; +``` + +### 3.7 Production Considerations + +- Socket.IO will automatically upgrade from polling to WebSocket +- Ensure load balancer supports WebSocket (sticky sessions required!) +- Consider Redis adapter for multi-server deployments (future) +- Monitor connection counts and memory usage +- Set up proper error handling for disconnections +- Consider implementing reconnection logic with exponential backoff --- diff --git a/plans/phase-3-server-js-changes.md b/plans/phase-3-server-js-changes.md new file mode 100644 index 0000000..26454a0 --- /dev/null +++ b/plans/phase-3-server-js-changes.md @@ -0,0 +1,130 @@ +# Phase 3: Exact Changes to server.js + +## Current server.js (lines 1-10) + +```javascript +import compression from "compression"; +import express from "express"; +import morgan from "morgan"; + +// Short-circuit the type-checking of the built output. +const BUILD_PATH = "./build/server/index.js"; +const DEVELOPMENT = process.env.NODE_ENV === "development"; +const PORT = Number.parseInt(process.env.PORT || "3000"); + +const app = express(); +``` + +## Add at top (after imports) + +```javascript +import compression from "compression"; +import express from "express"; +import morgan from "morgan"; +import { createServer } from "http"; // ← ADD THIS LINE + +// Short-circuit the type-checking of the built output. +const BUILD_PATH = "./build/server/index.js"; +const DEVELOPMENT = process.env.NODE_ENV === "development"; +const PORT = Number.parseInt(process.env.PORT || "3000"); + +const app = express(); +``` + +## Current server.js (end of file) + +```javascript + app.use(await import(BUILD_PATH).then((mod) => mod.app)); +} + +app.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); +}); +``` + +## Replace with (end of file) + +```javascript + app.use(await import(BUILD_PATH).then((mod) => mod.app)); +} + +// ← ADD THESE LINES ↓ +const httpServer = createServer(app); + +// Initialize Socket.IO +const { initializeSocketIO } = await import("./server/socket.js"); +initializeSocketIO(httpServer); + +httpServer.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); +}); +// ← END OF ADDITIONS +``` + +## Complete Diff + +```diff + import compression from "compression"; + import express from "express"; + import morgan from "morgan"; ++import { createServer } from "http"; + + // Short-circuit the type-checking of the built output. + const BUILD_PATH = "./build/server/index.js"; + const DEVELOPMENT = process.env.NODE_ENV === "development"; + const PORT = Number.parseInt(process.env.PORT || "3000"); + + const app = express(); + + app.use(compression()); + app.disable("x-powered-by"); + + if (DEVELOPMENT) { + console.log("Starting development server"); + const viteDevServer = await import("vite").then((vite) => + vite.createServer({ + server: { middlewareMode: true }, + }), + ); + app.use(viteDevServer.middlewares); + app.use(async (req, res, next) => { + try { + const source = await viteDevServer.ssrLoadModule("./server/app.ts"); + return await source.app(req, res, next); + } catch (error) { + if (typeof error === "object" && error instanceof Error) { + viteDevServer.ssrFixStacktrace(error); + } + next(error); + } + }); + } else { + console.log("Starting production server"); + app.use( + "/assets", + express.static("build/client/assets", { immutable: true, maxAge: "1y" }), + ); + app.use(morgan("tiny")); + app.use(express.static("build/client", { maxAge: "1h" })); + app.use(await import(BUILD_PATH).then((mod) => mod.app)); + } + +-app.listen(PORT, () => { ++const httpServer = createServer(app); ++ ++// Initialize Socket.IO ++const { initializeSocketIO } = await import("./server/socket.js"); ++initializeSocketIO(httpServer); ++ ++httpServer.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); + }); +``` + +## Summary + +**Lines Added:** 5 +**Lines Changed:** 1 +**Total Changes:** 6 lines + +This is the absolute minimum change needed to support Socket.IO while keeping the existing architecture intact. diff --git a/plans/phase-3-socket-implementation-plan.md b/plans/phase-3-socket-implementation-plan.md new file mode 100644 index 0000000..54ab3a9 --- /dev/null +++ b/plans/phase-3-socket-implementation-plan.md @@ -0,0 +1,278 @@ +# Phase 3: Socket.ts Implementation Plan - Avoiding Build Issues + +## The Problem + +React Router v7 bundles `server/app.ts` into `build/server/index.js`. If we're not careful, Socket.IO code could: +1. Get bundled into the server build (causing issues) +2. Try to import from bundled code (import path problems) +3. Create circular dependencies +4. Break HMR in development + +## Solution Strategy + +### Option 1: Keep Socket.IO OUTSIDE the Bundle (Recommended) ✅ + +**Structure:** +``` +/server.js <- Entry point (not bundled) +/server/ + socket.js <- Socket.IO code (not bundled, plain JS) + app.ts <- React Router app (gets bundled) +``` + +**Why this works:** +- `server.js` is the entry point and NOT bundled by React Router +- `server/socket.js` is imported by `server.js`, not by React Router code +- No circular dependencies +- Socket.IO stays completely separate from React Router's build + +### Option 2: Use Dynamic Import (Alternative) + +Keep Socket.IO in TypeScript but use dynamic imports to prevent bundling: + +```javascript +// In server.js +const { initializeSocketIO } = await import("./server/socket.js"); +``` + +This ensures Socket.IO is loaded at runtime, not bundled. + +## Detailed Implementation Plan + +### Step 1: Create server/socket.js (JavaScript, not TypeScript) + +**File: `server/socket.js`** + +```javascript +// Use CommonJS or ES modules based on your Node version +import { Server as SocketIOServer } from "socket.io"; + +let io = null; + +export function initializeSocketIO(httpServer) { + if (io) { + console.log("Socket.IO already initialized"); + return io; + } + + // Create Socket.IO server + io = new SocketIOServer(httpServer, { + // CORS only needed if clients connect from different origin + cors: process.env.NODE_ENV === "production" && process.env.APP_URL + ? { + origin: process.env.APP_URL, + credentials: true, + } + : undefined, // No CORS in dev (same origin) + }); + + // Connection handling + io.on("connection", (socket) => { + console.log("Client connected:", socket.id); + + // Join draft room + socket.on("join-draft", (seasonId) => { + if (!seasonId) { + console.error("No seasonId provided for join-draft"); + return; + } + socket.join(`draft-${seasonId}`); + console.log(`Socket ${socket.id} joined draft-${seasonId}`); + }); + + // Leave draft room + socket.on("leave-draft", (seasonId) => { + if (!seasonId) return; + socket.leave(`draft-${seasonId}`); + console.log(`Socket ${socket.id} left draft-${seasonId}`); + }); + + socket.on("disconnect", () => { + console.log("Client disconnected:", socket.id); + }); + }); + + // Store globally for access in route handlers + // This is safe because server.js controls the lifecycle + global.__socketIO = io; + + console.log("Socket.IO initialized on", httpServer.address()); + return io; +} + +export function getSocketIO() { + // Check both local and global storage + const instance = io || global.__socketIO; + if (!instance) { + throw new Error("Socket.IO not initialized. Call initializeSocketIO first."); + } + return instance; +} +``` + +### Step 2: Create TypeScript definitions + +**File: `server/socket.d.ts`** (Type definitions for socket.js) + +```typescript +import type { Server as HTTPServer } from "http"; +import type { Server as SocketIOServer } from "socket.io"; + +export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer; +export function getSocketIO(): SocketIOServer; +``` + +**File: `server/types.d.ts`** (Global type augmentation) + +```typescript +import type { Server as SocketIOServer } from "socket.io"; + +declare global { + var __socketIO: SocketIOServer | undefined; +} + +export {}; +``` + +### Step 3: Update server.js + +```javascript +import compression from "compression"; +import express from "express"; +import morgan from "morgan"; +import { createServer } from "http"; + +const BUILD_PATH = "./build/server/index.js"; +const DEVELOPMENT = process.env.NODE_ENV === "development"; +const PORT = Number.parseInt(process.env.PORT || "3000"); + +const app = express(); + +app.use(compression()); +app.disable("x-powered-by"); + +// ... existing middleware setup ... + +// Create HTTP server +const httpServer = createServer(app); + +// Initialize Socket.IO AFTER creating HTTP server +// Dynamic import to ensure it's not bundled +const { initializeSocketIO } = await import("./server/socket.js"); +initializeSocketIO(httpServer); + +// Use httpServer.listen instead of app.listen +httpServer.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); +}); +``` + +### Step 4: Access Socket.IO from React Router Routes + +**Option A: Direct Import (May have path issues)** +```typescript +// In app/routes/api.draft-pick.ts +import { getSocketIO } from "../../server/socket.js"; + +export async function action({ request }: ActionFunctionArgs) { + // Make database changes... + + // Broadcast update + const io = getSocketIO(); + io.to(`draft-${seasonId}`).emit("pick-made", data); + + return json({ success: true }); +} +``` + +**Option B: Through Context (Cleaner but more setup)** +```typescript +// In server/app.ts +import { getSocketIO } from "./socket.js"; + +// Add to loader context +export function getLoadContext() { + return { + getSocketIO, // Pass function reference + }; +} + +// In route +export async function action({ request, context }: ActionFunctionArgs) { + const io = context.getSocketIO(); + io.to(`draft-${seasonId}`).emit("pick-made", data); +} +``` + +## Build Process Verification + +### Development Mode Testing +```bash +npm run dev +# Check console for "Socket.IO initialized" +# Verify no build errors +# Test HMR still works +``` + +### Production Build Testing +```bash +npm run build +# Check build/server/index.js +# Socket.IO code should NOT be in the bundle +# Only server/app.ts code should be bundled + +# Run production +NODE_ENV=production node server.js +# Verify Socket.IO initializes +``` + +## Potential Issues & Solutions + +### Issue 1: Import path resolution +**Problem:** Can't import `server/socket.js` from React Router routes +**Solution:** Use global access pattern or pass through context + +### Issue 2: TypeScript errors +**Problem:** TS can't find types for `server/socket.js` +**Solution:** Create `server/socket.d.ts` with type definitions + +### Issue 3: HMR breaks Socket.IO +**Problem:** Hot reloads create multiple Socket.IO instances +**Solution:** The `if (io)` check prevents re-initialization + +### Issue 4: Build includes Socket.IO +**Problem:** Socket.IO gets bundled into build/server/index.js +**Solution:** Keep as .js file, use dynamic import in server.js + +## Why .js Instead of .ts? + +1. **No bundling:** .js files aren't processed by React Router's build +2. **Simple imports:** No build step needed for server.js to import it +3. **Clear separation:** Socket.IO logic separate from React Router app +4. **Runtime loading:** Loaded when server.js runs, not at build time + +## Alternative: Using TypeScript + +If you really want TypeScript for Socket.IO: + +1. Create `server/socket.ts` +2. Add a separate build step: `tsc server/socket.ts --outDir dist` +3. Import from `dist/socket.js` in server.js +4. Add to package.json scripts: + ```json + "build:socket": "tsc server/socket.ts --outDir dist", + "build": "npm run build:socket && react-router build" + ``` + +But this adds complexity for minimal benefit. + +## Recommended Approach + +✅ **Use `server/socket.js` (plain JavaScript)** +- Simpler +- No build step +- No bundling issues +- Still get type safety through .d.ts files +- Industry standard approach + +This keeps Socket.IO completely separate from React Router's build process while maintaining type safety and avoiding all bundling issues. diff --git a/plans/phase-3-socket-implementation-summary.md b/plans/phase-3-socket-implementation-summary.md new file mode 100644 index 0000000..bec225c --- /dev/null +++ b/plans/phase-3-socket-implementation-summary.md @@ -0,0 +1,111 @@ +# Phase 3: Socket.IO Implementation - Summary + +## Key Decision: Modify server.js (Option A) + +After researching React Router v7 + Socket.IO integration patterns, the recommended approach is: + +### Why This Approach? + +1. **Industry Standard**: Socket.IO requires `http.createServer(app)` - this is the documented pattern +2. **Single Port**: HTTP and WebSocket traffic on same port (simpler deployment) +3. **No CORS Issues**: Everything runs on same origin +4. **Works with SSR**: Compatible with React Router v7's server-side rendering +5. **Simpler Deployment**: One process, one port, one service + +### What Changes? + +**Minimal changes to `server.js`:** +```javascript +// Before: +app.listen(PORT, () => {...}); + +// After: +import { createServer } from "http"; +const httpServer = createServer(app); +const { initializeSocketIO } = await import("./server/socket.js"); +initializeSocketIO(httpServer); +httpServer.listen(PORT, () => {...}); +``` + +That's it! Just 4 lines changed in server.js. + +## Alternative Considered: Separate Socket Server (Option B) + +**Why we rejected this:** +- Requires managing two processes +- CORS configuration complexity +- Different ports (3000 for HTTP, 3001 for WebSocket) +- More complex deployment (two services) +- Not the standard pattern + +**When to use Option B:** +- If you absolutely cannot modify server.js +- If you need to scale WebSocket separately from HTTP +- If you're using a microservices architecture + +## Implementation Steps + +### Step 1: Create Socket Module +- File: `server/socket.js` (not .ts to avoid bundling) +- Exports: `initializeSocketIO()` and `getSocketIO()` +- Stores instance globally for route handlers + +### Step 2: Modify server.js +- Import `http.createServer` +- Create HTTP server from Express app +- Initialize Socket.IO with HTTP server +- Call `httpServer.listen()` instead of `app.listen()` + +### Step 3: Client Hook +- Create `app/hooks/useDraftSocket.ts` +- Handles connection/disconnection +- Auto-joins draft room on connect +- Provides `on`/`off` helpers for events + +### Step 4: Route Handlers +- Import `getSocketIO()` in API routes +- Emit events after database writes +- Example: After creating pick, emit `pick-made` to room + +## Event Architecture + +**Write Operations → HTTP POST** +- Making picks +- Queue management +- Commissioner actions +- Reason: Reliability, error handling, validation + +**Read Operations → Socket.IO Broadcast** +- Pick made notifications +- Timer updates +- Draft status changes +- Reason: Real-time updates to all clients + +## Testing Checklist + +- [ ] Server starts with "Socket.IO initialized" +- [ ] Browser console shows "Connected to Socket.IO" +- [ ] Server logs show "Client connected: [id]" +- [ ] Multiple clients can join same draft room +- [ ] Picks broadcast to all clients in room +- [ ] Disconnection handled gracefully + +## Production Notes + +- Socket.IO auto-upgrades from polling → WebSocket +- Load balancer needs sticky sessions for WebSocket +- Monitor connection counts +- Consider Redis adapter for horizontal scaling (future) + +## Files Created/Modified + +**New Files:** +- `server/socket.js` - Socket.IO initialization +- `server/types.d.ts` - TypeScript globals +- `app/hooks/useDraftSocket.ts` - Client hook + +**Modified Files:** +- `server.js` - 4 lines changed (http.createServer) +- `tsconfig.node.json` - Add server directory + +**Total LOC:** ~150 lines of new code