# Migration Example: Using the Pluggable Architecture This document provides concrete examples of how to use the new pluggable architecture, demonstrating both backward compatibility and new patterns. ## Table of Contents 1. [Using the Adapter for Backward Compatibility](#using-the-adapter-for-backward-compatibility) 2. [Writing New Code with Metadata Pattern](#writing-new-code-with-metadata-pattern) 3. [Creating a New Application Type](#creating-a-new-application-type) 4. [Migration Path](#migration-path) ## Using the Adapter for Backward Compatibility The adapter layer allows existing code to work unchanged while the codebase gradually migrates to the new architecture. ### Example: Existing Code Continues to Work ```typescript import { wrapGame, wrapSession } from './routes/games/gameAdapter'; import type { Game } from './routes/games/types'; // Load existing game from database/storage const loadedGame: Game = loadGameFromDatabase(gameId); // Wrap the game object to enable backward compatibility const game = wrapGame(loadedGame); // Old code continues to work unchanged! game.turn = { color: "red", roll: 7 }; // Proxied to game.metadata.turn game.players["red"].resources = 5; // Proxied to game.metadata.players["red"] // Session operations also work const session = game.sessions["session123"]; session.color = "blue"; // Proxied to session.metadata.color session.player = game.players["blue"]; // Proxied to session.metadata.player console.log("Player color:", session.color); // Reads from session.metadata.color console.log("Turn data:", game.turn); // Reads from game.metadata.turn ``` ### How the Proxy Works The adapter uses JavaScript Proxy to intercept property access: ```typescript // Reading game-specific properties const color = session.color; // Behind the scenes: session.metadata?.color // Writing game-specific properties session.color = "red"; // Behind the scenes: session.metadata = { ...session.metadata, color: "red" } // Infrastructure properties work normally const sessionId = session.id; // Direct access (not proxied) const isLive = session.live; // Direct access (not proxied) ``` ## Writing New Code with Metadata Pattern New code should use the metadata pattern directly for clean separation of concerns. ### Example: Session Color Selection (New Pattern) ```typescript import type { GameSession, GameSessionMetadata } from './routes/games/gameMetadata'; import { updateSessionMetadata, getSessionMetadata } from './routes/room/helpers'; function handleColorSelection(session: GameSession, color: string): void { // Update game-specific metadata updateSessionMetadata(session, { color: color }); // Infrastructure fields remain separate session.live = true; session.lastActive = Date.now(); } function getPlayerColor(session: GameSession): string | undefined { const metadata = getSessionMetadata(session); return metadata?.color; } ``` ### Example: Getting Participants with Game Data ```typescript import { getParticipants } from './routes/room/helpers'; import type { GameRoom, GameSessionMetadata } from './routes/games/gameMetadata'; function getGameParticipants(room: GameRoom) { // Get base participant data (reusable) const baseParticipants = getParticipants(room.sessions); // Extend with game-specific data return baseParticipants.map(p => { const session = room.sessions[p.session_id]; const metadata = session.metadata as GameSessionMetadata | undefined; return { ...p, // Base participant data color: metadata?.color || null, // Game-specific data }; }); } ``` ### Example: Room State Management ```typescript import type { GameRoom, GameRoomMetadata } from './routes/games/gameMetadata'; import { updateRoomMetadata, getRoomMetadata } from './routes/room/helpers'; function initializeGameRoom(room: GameRoom): void { // Initialize game-specific metadata updateRoomMetadata(room, { players: {}, turn: { color: undefined, actions: [] }, placements: { corners: [], roads: [] }, chat: [], developmentCards: [], }); // Infrastructure state remains separate room.state = "lobby"; } function updateTurn(room: GameRoom, color: string, roll: number): void { const metadata = getRoomMetadata(room); if (!metadata) return; // Update game state in metadata metadata.turn = { ...metadata.turn, color, roll, }; // Infrastructure state changes separately room.state = "playing"; } ``` ## Creating a New Application Type The infrastructure can be reused for any multi-user WebRTC application. ### Example: Creating a Whiteboard Application ```typescript // Step 1: Define your metadata types // File: server/routes/whiteboard/whiteboardMetadata.ts import type { Session, Room } from '../room/types'; import type { Player } from './whiteboardTypes'; export interface WhiteboardSessionMetadata { tool: "pen" | "eraser" | "shape"; color: string; brushSize: number; selectedObject?: string; } export interface WhiteboardRoomMetadata { canvas: CanvasObject[]; history: HistoryEntry[]; locked: boolean; } export type WhiteboardSession = Session; export type WhiteboardRoom = Room; ``` ```typescript // Step 2: Use the reusable infrastructure // File: server/routes/whiteboard/handlers.ts import type { WhiteboardRoom, WhiteboardSession, WhiteboardSessionMetadata } from './whiteboardMetadata'; import { getParticipants, updateSessionMetadata, updateRoomMetadata } from '../room/helpers'; export function handleToolChange( session: WhiteboardSession, tool: "pen" | "eraser" | "shape" ): void { updateSessionMetadata(session, { tool }); } export function getWhiteboardParticipants(room: WhiteboardRoom) { const baseParticipants = getParticipants(room.sessions); // Add whiteboard-specific data return baseParticipants.map(p => { const session = room.sessions[p.session_id]; const metadata = session.metadata; return { ...p, tool: metadata?.tool || "pen", color: metadata?.color || "#000000", brushSize: metadata?.brushSize || 5, }; }); } export function addCanvasObject(room: WhiteboardRoom, object: CanvasObject): void { if (!room.metadata) { updateRoomMetadata(room, { canvas: [], history: [], locked: false, }); } room.metadata!.canvas.push(object); room.metadata!.history.push({ type: 'add', timestamp: Date.now(), object, }); } ``` ```typescript // Step 3: Reuse MediaControl component (client-side) // File: client/src/WhiteboardApp.tsx import { MediaAgent, MediaControl } from './MediaControl'; import type { Session } from './GlobalContext'; function WhiteboardApp() { const [session, setSession] = useState(null); const [peers, setPeers] = useState>({}); return ( <> {/* Reuse MediaAgent - no changes needed! */} {/* Reuse MediaControl - no changes needed! */} {participants.map(p => ( ))} {/* Your whiteboard-specific UI */} ); } ``` ## Migration Path ### Phase 1: Add Metadata Layer (✅ Complete) The infrastructure and metadata types are now defined. Code can use either pattern: ```typescript // Old pattern (via adapter) session.color = "red"; // New pattern (direct metadata access) updateSessionMetadata(session, { color: "red" }); ``` ### Phase 2: Migrate Functions One by One Gradually update functions to use metadata pattern: ```typescript // Before: Mixed concerns function setPlayerColor(session: any, color: string): void { session.color = color; // Direct property access session.live = true; } // After: Separated concerns function setPlayerColor(session: GameSession, color: string): void { updateSessionMetadata(session, { color }); // Metadata session.live = true; // Infrastructure } ``` ### Phase 3: Update Message Handlers Update WebSocket message handlers to work with metadata: ```typescript // Before case "set": { if (data.field === "color") { session.color = data.value; } break; } // After case "set": { if (data.field === "color") { updateSessionMetadata(session, { color: data.value }); } break; } ``` ### Phase 4: Remove Adapters (Future) Once all code is migrated, remove the adapter layer and use pure metadata: ```typescript // Pure metadata architecture (no adapters) const session: GameSession = { // Infrastructure fields id: "abc123", name: "Alice", live: true, has_media: true, lastActive: Date.now(), // Application-specific data metadata: { color: "red", player: {...}, resources: 5, } }; ``` ## Benefits of the New Architecture ### 1. Reusability The same infrastructure works for different applications: ```typescript // Game application const gameRoom: Room = {...}; // Whiteboard application const whiteboardRoom: Room = {...}; // Chat application const chatRoom: Room = {...}; // All use the same getParticipants, session management, WebRTC signaling! ``` ### 2. Type Safety TypeScript ensures correct metadata usage: ```typescript const session: GameSession = {...}; // ✅ Type-safe updateSessionMetadata(session, { color: "red" }); // ❌ TypeScript error: 'invalidField' doesn't exist updateSessionMetadata(session, { invalidField: "value" }); ``` ### 3. Clear Separation Infrastructure and application logic are clearly separated: ```typescript // Infrastructure (works for any app) import { getParticipants, updateSessionActivity } from './room/helpers'; // Application-specific (game logic) import { GameSession, GameRoom } from './games/gameMetadata'; import { calculatePoints, updateTurn } from './games/gameLogic'; ``` ### 4. Easy Testing Each layer can be tested independently: ```typescript // Test infrastructure describe('getParticipants', () => { it('returns participant list from any session type', () => { const sessions = { /* any session structure */ }; const participants = getParticipants(sessions); expect(participants).toHaveLength(2); }); }); // Test game logic describe('calculatePoints', () => { it('calculates game points from metadata', () => { const room: GameRoom = { /* ... */ }; calculatePoints(room); expect(room.metadata?.players['red'].points).toBe(10); }); }); ``` ## Common Patterns ### Pattern 1: Extending Participant Lists ```typescript function getExtendedParticipants>( room: Room, extender: (session: Session) => TMetadata ) { const baseParticipants = getParticipants(room.sessions); return baseParticipants.map(p => ({ ...p, ...extender(room.sessions[p.session_id]) })); } // Usage const gameParticipants = getExtendedParticipants(gameRoom, (session) => ({ color: session.metadata?.color || null, points: session.metadata?.player?.points || 0, })); ``` ### Pattern 2: Metadata Initialization ```typescript function ensureSessionMetadata( session: Session, defaults: TMetadata ): void { if (!session.metadata) { session.metadata = defaults; } } // Usage ensureSessionMetadata(session, { color: undefined, player: undefined, resources: 0, }); ``` ### Pattern 3: Safe Metadata Access ```typescript function getMetadataField( session: Session, field: K, defaultValue: TMetadata[K] ): TMetadata[K] { return session.metadata?.[field] ?? defaultValue; } // Usage const color = getMetadataField( session, 'color', 'gray' ); ``` ## Summary The new architecture provides: - **Clean separation** between infrastructure and application logic - **Reusable components** (MediaControl, Room helpers, WebRTC signaling) - **Type safety** with generic TypeScript types - **Backward compatibility** via adapter pattern - **Extensibility** for new application types By following these patterns, you can create new applications that leverage the existing WebRTC and Room infrastructure without modification.