* Update claude.md to push more tests. * Archiving old plans. * fix: run DB migrations programmatically on server startup Replace unreliable drizzle-kit CLI migration with drizzle-orm's built-in migrator running in the server process before accepting connections. This ensures migrations are always applied on deploy and fails fast if they error. - Add runMigrations() to server.ts using drizzle-orm/postgres-js/migrator - Skip migrations in development (handled manually via db:migrate) - Add DATABASE_URL guard with a clear error message - Remove start:production script (now identical to start) - Update Dockerfile CMD to use npm run start Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
278 lines
7.5 KiB
Markdown
278 lines
7.5 KiB
Markdown
# 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.
|