1
0
peddlers-of-ketran/ARCHITECTURE.md

13 KiB

Pluggable Room/WebRTC Architecture

This document describes the layered architecture that separates reusable Room/WebRTC infrastructure from application-specific logic.

Overview

The system is designed with three distinct layers:

  1. Infrastructure Layer - Reusable Room/Session/WebRTC management
  2. Metadata Layer - Application-specific data attached to infrastructure
  3. Adapter Layer - Backward compatibility and migration support

This separation allows MediaControl and Room management to be reused across different applications without modification.

Architecture Diagram

┌─────────────────────────────────────────────────────────────┐
│                     Application Layer                        │
│         (Game Logic - Settlers of Catan specific)           │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  Game Rules, Turns, Resources, Trading, etc.           │ │
│  └────────────────────────────────────────────────────────┘ │
└────────────────────┬────────────────────────────────────────┘
                     │ uses metadata
┌────────────────────▼────────────────────────────────────────┐
│                    Metadata Layer                            │
│         (Application-specific data structures)              │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  Session.metadata { color, player, resources }         │ │
│  │  Room.metadata { game state, board, cards, etc. }     │ │
│  └────────────────────────────────────────────────────────┘ │
└────────────────────┬────────────────────────────────────────┘
                     │ attached to
┌────────────────────▼────────────────────────────────────────┐
│                Infrastructure Layer                          │
│            (Reusable across applications)                   │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  BaseSession: id, name, ws, live, has_media           │ │
│  │  BaseRoom: id, sessions, state                        │ │
│  │  MediaControl: WebRTC signaling, peer management      │ │
│  │  Room Helpers: getParticipants, session management    │ │
│  └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Type Hierarchy

Infrastructure Types (Reusable)

Located in: server/routes/room/types.ts

// Core session - no application logic
interface BaseSession {
  id: string;
  name: string | null;
  ws?: WebSocket;
  live: boolean;
  has_media: boolean;
  // ... infrastructure fields only
}

// Core room - no application logic
interface BaseRoom {
  id: string;
  sessions: Record<string, BaseSession>;
  state: string;
  // ... infrastructure fields only
}

// Generic with metadata support
interface Session<TMetadata = any> extends BaseSession {
  metadata?: TMetadata; // Application data goes here
}

interface Room<TMetadata = any> extends BaseRoom {
  metadata?: TMetadata; // Application data goes here
}

Application-Specific Types

Located in: server/routes/games/gameMetadata.ts

// Game-specific session data
interface GameSessionMetadata {
  color?: string;        // Player color selection
  player?: Player;       // Reference to game player
  resources?: number;    // Temporary resource count
}

// Game-specific room data
interface GameRoomMetadata {
  players: Record<string, Player>;  // Game players
  turn: Turn;                       // Current turn
  placements: Placements;           // Board state
  chat: any[];                      // Chat messages
  // ... all game-specific fields
}

// Convenience types
type GameSession = Session<GameSessionMetadata>;
type GameRoom = Room<GameRoomMetadata>;

Key Principles

1. Clear Separation of Concerns

Infrastructure Layer Responsibilities:

  • WebSocket connection management
  • Session lifecycle (connect, disconnect, timeout)
  • WebRTC signaling (join, peer, ICE, SDP)
  • Participant listing
  • Room state management

Metadata Layer Responsibilities:

  • Application-specific data structures
  • Business logic
  • Game rules and validation
  • Application state

2. Metadata Pattern

All application-specific data is stored in metadata fields:

// ❌ Old approach - mixed concerns
interface Session {
  id: string;          // Infrastructure
  name: string;        // Infrastructure
  color: string;       // APPLICATION SPECIFIC! ❌
  player: Player;      // APPLICATION SPECIFIC! ❌
}

// ✅ New approach - separated concerns
interface Session<TMetadata> {
  id: string;          // Infrastructure
  name: string;        // Infrastructure
  metadata?: TMetadata; // Application-specific data
}

// Usage
const session: Session<GameSessionMetadata> = {
  id: "abc123",
  name: "Alice",
  metadata: {
    color: "red",      // Game-specific
    player: {...}      // Game-specific
  }
};

3. Extensibility

Any application can use the infrastructure by defining their own metadata types:

// Example: Chat application
interface ChatSessionMetadata {
  displayColor: string;
  status: "available" | "away" | "busy";
  lastTyping?: number;
}

type ChatSession = Session<ChatSessionMetadata>;

// Example: Whiteboard application
interface WhiteboardSessionMetadata {
  tool: "pen" | "eraser" | "shape";
  color: string;
  brushSize: number;
}

type WhiteboardSession = Session<WhiteboardSessionMetadata>;

Reusable Components

MediaControl (Client)

Located in: client/src/MediaControl.tsx

Reusable: No application-specific logic

// MediaControl only needs infrastructure data
<MediaControl
  peer={peer}              // Has: session_id, peer_name, srcObject
  isSelf={isLocal}
  sendJsonMessage={send}
/>

// MediaAgent only needs infrastructure
<MediaAgent
  session={session}        // Has: id, name, has_media
  socketUrl={url}
  peers={peers}
  setPeers={setPeers}
/>

Room Helpers (Server)

Located in: server/routes/room/helpers.ts

Reusable: Generic functions work with any metadata

import { getParticipants } from './room/helpers';

// Works with any Room<T>
const participants = getParticipants(room.sessions);

// Returns base participant data (no app-specific fields)
// [{ session_id, name, live, has_media, ... }]

Extending Participants

Applications can extend the base participant list:

// Get base participants (reusable)
const baseParticipants = getParticipants(room.sessions);

// Add application-specific data
const gameParticipants = baseParticipants.map(p => ({
  ...p,
  color: room.sessions[p.session_id].metadata?.color || null
}));

Migration Strategy

Phase 1: Adapter Layer (Current)

Use the adapter layer to maintain backward compatibility:

import { wrapGame, wrapSession } from './games/gameAdapter';

// Wrap existing game objects
const game = wrapGame(loadedGame);

// Code still works with old syntax
session.color = "red";  // Proxied to session.metadata.color
game.turn = {...};      // Proxied to game.metadata.turn

Phase 2: Gradual Migration

Migrate functions one at a time:

// Old code
function doSomething(game: any) {
  const color = session.color;  // Direct access
}

// New code
function doSomething(game: GameRoom) {
  const color = session.metadata?.color;  // Metadata access
}

Phase 3: Pure Architecture

Eventually remove adapters and use pure metadata architecture:

// All application data in metadata
const session: GameSession = {
  id: "123",
  name: "Alice",
  has_media: true,
  live: true,
  // ... base fields
  metadata: {
    color: "red",
    player: {...},
    resources: 5
  }
};

Usage Examples

Example 1: Creating a New Room Type

// Define your metadata
interface MyAppSessionMetadata {
  customField: string;
}

interface MyAppRoomMetadata {
  appState: any;
}

// Use the base types
type MySession = Session<MyAppSessionMetadata>;
type MyRoom = Room<MyAppRoomMetadata>;

// Reuse infrastructure
import { getParticipants } from './room/helpers';

function listUsers(room: MyRoom) {
  return getParticipants(room.sessions);
}

Example 2: Extending Participant Data

// Start with base participants
const participants = getParticipants(room.sessions);

// Add app-specific fields
const extendedParticipants = participants.map(p => {
  const session = room.sessions[p.session_id];
  return {
    ...p,
    // Add your custom fields
    customField: session.metadata?.customField,
  };
});

Example 3: MediaControl Integration

// MediaControl is fully reusable
import { MediaAgent, MediaControl } from './MediaControl';

function MyApp() {
  const [peers, setPeers] = useState<Record<string, Peer>>({});

  return (
    <>
      <MediaAgent
        session={session}      // Base session data
        socketUrl={socketUrl}
        peers={peers}
        setPeers={setPeers}
      />

      {participants.map(p => (
        <MediaControl
          peer={peers[p.session_id]}
          isSelf={p.session_id === session.id}
        />
      ))}
    </>
  );
}

Benefits

1. Reusability

  • MediaControl can be used in any WebRTC application
  • Room management works for any multi-user application
  • WebSocket handling is application-agnostic

2. Maintainability

  • Clear boundaries between infrastructure and application
  • Easy to test each layer independently
  • Simpler upgrade paths

3. Type Safety

  • Generic types ensure type safety across layers
  • TypeScript enforces proper metadata usage
  • IntelliSense works correctly

4. Flexibility

  • Easy to add new applications using same infrastructure
  • Can run multiple different applications on same server
  • Minimal code changes to adapt to new use cases

File Organization

server/
├── routes/
│   ├── room/                    # Reusable infrastructure
│   │   ├── types.ts            # Base types (BaseSession, BaseRoom, etc.)
│   │   └── helpers.ts          # Reusable functions
│   │
│   └── games/                   # Game-specific code
│       ├── types.ts            # Game domain types (Player, Turn, etc.)
│       ├── gameMetadata.ts     # Metadata type definitions
│       ├── gameAdapter.ts      # Backward compatibility
│       └── helpers.ts          # Game-specific logic

client/
└── src/
    ├── MediaControl.tsx         # Reusable WebRTC component
    └── PlayerList.tsx          # Uses MediaControl + game metadata

Best Practices

DO:

Keep infrastructure layer generic and reusable Put all application logic in metadata Use type parameters for extensibility Document what's reusable vs. application-specific Test infrastructure independently

DON'T:

Mix application logic into infrastructure types Hard-code application fields in base types Duplicate infrastructure code per application Bypass metadata layer in new code Create circular dependencies between layers

Next Steps

  1. Immediate: Use adapter layer for backward compatibility
  2. Short-term: Migrate high-traffic functions to use metadata
  3. Long-term: Remove adapters, use pure metadata architecture
  4. Future: Extract infrastructure into separate npm package

See Also