brackt/plans/completed/phase-3-socket-implementation-plan.md

279 lines
7.5 KiB
Markdown
Raw Normal View History

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