376 lines
11 KiB
Markdown
376 lines
11 KiB
Markdown
# 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)
|
|
|
|
**[server/routes/room/types.ts](server/routes/room/types.ts)**
|
|
- `BaseSession`: Core session data (id, name, ws, live, has_media)
|
|
- `Session<TMetadata>`: Generic session with app-specific metadata
|
|
- `BaseRoom`: Core room data (id, name, sessions, state)
|
|
- `Room<TMetadata>`: Generic room with app-specific metadata
|
|
- `Participant`: Minimal info for participant lists
|
|
- `PeerConfig`, `PeerRegistry`: WebRTC peer management
|
|
|
|
**[server/routes/room/helpers.ts](server/routes/room/helpers.ts)**
|
|
- `getParticipants()`: Get participant list from sessions
|
|
- `createBaseSession()`: Create new session
|
|
- `getSessionName()`: Get display name
|
|
- `updateSessionActivity()`: Update last active timestamp
|
|
- `isSessionActive()`: Check if session is active
|
|
- `getActiveSessions()`: Filter active sessions
|
|
- `cleanupInactiveSessions()`: Remove stale sessions
|
|
|
|
**[client/src/MediaControl.tsx](client/src/MediaControl.tsx)**
|
|
- `MediaAgent`: WebRTC signaling and connection management
|
|
- `MediaControl`: Video feed and controls UI
|
|
- Works with `Participant` type from any application
|
|
|
|
### Application Layer (Game-Specific)
|
|
|
|
**[server/routes/games/gameMetadata.ts](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 metadata
|
|
- `GameRoom`: Room type with game metadata
|
|
- Migration helpers between old and new formats
|
|
|
|
**[server/routes/games/types.ts](server/routes/games/types.ts)**
|
|
- `Player`: Player game state
|
|
- `Turn`, `Placements`, `DevelopmentCard`: Game-specific types
|
|
- (Legacy `Session` and `Game` types - will be deprecated)
|
|
|
|
### Adapter Layer (Backward Compatibility)
|
|
|
|
**[server/routes/games/gameAdapter.ts](server/routes/games/gameAdapter.ts)**
|
|
- `wrapSession()`: Proxy for transparent metadata access
|
|
- `wrapGame()`: Proxy for transparent metadata access
|
|
- Allows code to use `session.color` instead of `session.metadata.color`
|
|
- Enables gradual migration without breaking existing code
|
|
|
|
## Type Definitions
|
|
|
|
### Infrastructure Types
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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)
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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 use `import { 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
|
|
|
|
1. **Cleaner Code**: Game logic is clearly separated from infrastructure
|
|
2. **Easier Testing**: Can mock sessions without game data
|
|
3. **Better Type Safety**: Explicit metadata types
|
|
4. **Maintainability**: Clear boundaries between layers
|
|
|
|
### For Reusability
|
|
|
|
1. **Drop-In WebRTC**: MediaControl works with any app
|
|
2. **Flexible Metadata**: Easy to add app-specific data
|
|
3. **Minimal Coupling**: Infrastructure has zero game dependencies
|
|
4. **Proven Patterns**: Well-documented extension points
|
|
|
|
## Current Implementation Status
|
|
|
|
- ✅ **Infrastructure types defined** ([server/routes/room/types.ts](server/routes/room/types.ts))
|
|
- ✅ **Helper functions created** ([server/routes/room/helpers.ts](server/routes/room/helpers.ts))
|
|
- ✅ **Game metadata types defined** ([server/routes/games/gameMetadata.ts](server/routes/games/gameMetadata.ts))
|
|
- ✅ **Adapter layer implemented** ([server/routes/games/gameAdapter.ts](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
|
|
|
|
```typescript
|
|
// 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
|