1
0
peddlers-of-ketran/PLUGGABLE_ARCHITECTURE.md

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)

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

  • 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

  • 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

  • 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

  • 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

  • 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

// 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 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

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