11 KiB
11 KiB
Pluggable Room/WebRTC Architecture
This document describes the clean separation between reusable Room/WebRTC infrastructure and application-specific game logic.
Architecture Overview
The system is now organized in three layers:
┌─────────────────────────────────────────────────────┐
│ Application Layer (Game-Specific) │
│ - Game rules, player colors, resources │
│ - Stored in metadata fields │
└─────────────────────────────────────────────────────┘
│
├── uses metadata
↓
┌─────────────────────────────────────────────────────┐
│ Adapter Layer (Backward Compatibility) │
│ - Proxies for transparent access │
│ - Maps old API to new architecture │
└─────────────────────────────────────────────────────┘
│
├── wraps
↓
┌─────────────────────────────────────────────────────┐
│ Infrastructure Layer (Reusable) │
│ - Room/Session management │
│ - WebSocket handling │
│ - WebRTC signaling (MediaControl) │
│ - Participant lists │
└─────────────────────────────────────────────────────┘
File Organization
Infrastructure Layer (Reusable)
BaseSession
: Core session data (id, name, ws, live, has_media)Session<TMetadata>
: Generic session with app-specific metadataBaseRoom
: Core room data (id, name, sessions, state)Room<TMetadata>
: Generic room with app-specific metadataParticipant
: Minimal info for participant listsPeerConfig
,PeerRegistry
: WebRTC peer management
getParticipants()
: Get participant list from sessionscreateBaseSession()
: Create new sessiongetSessionName()
: Get display nameupdateSessionActivity()
: Update last active timestampisSessionActive()
: Check if session is activegetActiveSessions()
: Filter active sessionscleanupInactiveSessions()
: Remove stale sessions
MediaAgent
: WebRTC signaling and connection managementMediaControl
: Video feed and controls UI- Works with
Participant
type from any application
Application Layer (Game-Specific)
server/routes/games/gameMetadata.ts
GameSessionMetadata
: Game-specific session data (color, player, resources)GameRoomMetadata
: Game-specific room data (players, board, rules)GameSession
: Session type with game metadataGameRoom
: Room type with game metadata- Migration helpers between old and new formats
Player
: Player game stateTurn
,Placements
,DevelopmentCard
: Game-specific types- (Legacy
Session
andGame
types - will be deprecated)
Adapter Layer (Backward Compatibility)
server/routes/games/gameAdapter.ts
wrapSession()
: Proxy for transparent metadata accesswrapGame()
: Proxy for transparent metadata access- Allows code to use
session.color
instead ofsession.metadata.color
- Enables gradual migration without breaking existing code
Type Definitions
Infrastructure Types
// Base Session (reusable)
interface BaseSession {
// Identity
id: string;
userId?: number;
name: string | null;
// Connection
ws?: WebSocket;
live: boolean;
lastActive: number;
connected: boolean;
// Media
has_media: boolean;
// Security/Bot
protected?: boolean;
bot_run_id?: string | null;
bot_provider_id?: string | null;
bot_instance_id?: string | null;
}
// Generic Session with metadata
interface Session<TMetadata = any> extends BaseSession {
metadata?: TMetadata; // Your app data goes here
}
// Participant (for UI lists)
interface Participant {
name: string | null;
session_id: string;
live: boolean;
has_media: boolean;
protected?: boolean;
// ... bot fields
}
Game-Specific Types
// Game session metadata
interface GameSessionMetadata {
color?: string; // Player color in game
player?: Player; // Reference to player object
resources?: number; // Temporary resource count
}
// Game room metadata
interface GameRoomMetadata {
players: Record<string, Player>; // Game players by color
developmentCards: DevelopmentCard[];
placements: Placements;
turn: Turn;
// ... all game-specific data
}
// Complete game session
type GameSession = Session<GameSessionMetadata>;
// Complete game room
type GameRoom = Room<GameRoomMetadata>;
Usage Examples
Creating a New Session (Infrastructure)
import { createBaseSession } from './room/helpers';
// Create base session (no game data)
const baseSession = createBaseSession('session-123', 'Alice');
// Add game-specific data
const gameSession: GameSession = {
...baseSession,
metadata: {
color: 'red',
player: gamePlayerObject,
}
};
Getting Participants (Reusable)
import { getParticipants } from './room/helpers';
// Get base participant list (works for any app)
const baseParticipants = getParticipants(room.sessions);
// Extend with game-specific data
const gameParticipants = baseParticipants.map(p => ({
...p,
color: room.sessions[p.session_id].metadata?.color,
}));
Using the Adapter (Backward Compatibility)
import { wrapSession, wrapGame } from './games/gameAdapter';
// Old code can still work
const session = wrapSession(gameSession);
console.log(session.color); // Accesses session.metadata.color transparently
const game = wrapGame(gameRoom);
console.log(game.players); // Accesses game.metadata.players transparently
Reusing for Another Application
To use the Room/WebRTC infrastructure for a different application:
1. Define Your Metadata Types
// myapp/metadata.ts
export interface MySessionMetadata {
score: number;
team: string;
role: string;
}
export interface MyRoomMetadata {
gameMode: string;
maxPlayers: number;
settings: any;
}
export type MySession = Session<MySessionMetadata>;
export type MyRoom = Room<MyRoomMetadata>;
2. Use Infrastructure Helpers
import { createBaseSession, getParticipants } from './room/helpers';
// Create session with your metadata
const session: MySession = {
...createBaseSession('user-456', 'Bob'),
metadata: {
score: 0,
team: 'blue',
role: 'defender',
}
};
// Get participants (works out of the box)
const participants = getParticipants(room.sessions);
3. Extend Participants with Your Data
function getMyAppParticipants(room: MyRoom) {
const base = getParticipants(room.sessions);
return base.map(p => ({
...p,
team: room.sessions[p.session_id].metadata?.team,
role: room.sessions[p.session_id].metadata?.role,
}));
}
4. Use MediaControl As-Is
// client/src/MyApp.tsx
import { MediaAgent, MediaControl } from './MediaControl';
// Works with your participant type
<MediaAgent
socketUrl={socketUrl}
session={session}
peers={peers}
setPeers={setPeers}
/>
Migration Path
Phase 1: Add Metadata Layer (Current)
- ✅ Create new types with metadata separation
- ✅ Create adapter layer for backward compatibility
- ✅ Document architecture
- Existing code continues to work unchanged
Phase 2: Gradual Migration (Future)
- Update
getParticipants()
to useimport { getParticipants } from './room/helpers'
- Move game logic to use
session.metadata.color
explicitly - Update session creation to use new format
- Remove adapter proxies where code is migrated
Phase 3: Complete Separation (Future)
- Extract room/WebRTC code to separate package
- Publish as reusable library
- Game code only depends on metadata types
- Full separation achieved
Benefits
For Current Game
- Cleaner Code: Game logic is clearly separated from infrastructure
- Easier Testing: Can mock sessions without game data
- Better Type Safety: Explicit metadata types
- Maintainability: Clear boundaries between layers
For Reusability
- Drop-In WebRTC: MediaControl works with any app
- Flexible Metadata: Easy to add app-specific data
- Minimal Coupling: Infrastructure has zero game dependencies
- Proven Patterns: Well-documented extension points
Current Implementation Status
- ✅ Infrastructure types defined (server/routes/room/types.ts)
- ✅ Helper functions created (server/routes/room/helpers.ts)
- ✅ Game metadata types defined (server/routes/games/gameMetadata.ts)
- ✅ Adapter layer implemented (server/routes/games/gameAdapter.ts)
- ✅ Documentation complete (this file)
- ⏳ Existing code uses adapter (backward compatible, no changes needed)
- ⏳ Gradual migration (future work, optional)
Example: Todo List App Using This Infrastructure
// todo-metadata.ts
interface TodoSessionMetadata {
completedCount: number;
role: 'viewer' | 'editor' | 'admin';
}
interface TodoRoomMetadata {
todos: Array<{ id: string; text: string; done: boolean }>;
createdBy: string;
}
// todo-app.ts
import { createBaseSession, getParticipants } from './room/helpers';
import { MediaAgent } from './MediaControl';
type TodoSession = Session<TodoSessionMetadata>;
type TodoRoom = Room<TodoRoomMetadata>;
// Create room
const room: TodoRoom = {
...createBaseRoom('room-123'),
metadata: {
todos: [],
createdBy: 'user-1',
}
};
// Add session
room.sessions['user-1'] = {
...createBaseSession('user-1', 'Alice'),
metadata: {
completedCount: 0,
role: 'admin',
}
};
// Get participants with WebRTC (works out of box)
const participants = getParticipants(room.sessions);
// Use MediaControl for video chat while working on todos
<MediaAgent session={session} peers={peers} setPeers={setPeers} />
The Todo app gets WebRTC/video for free, with clean separation!
Summary
This architecture provides:
- ✅ Clean separation between infrastructure and application
- ✅ Reusable components (Room, Session, MediaControl, WebRTC)
- ✅ Type-safe metadata for application-specific data
- ✅ Backward compatibility via adapter layer
- ✅ Easy migration path with no breaking changes
- ✅ Well-documented extension points for new applications