# 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.