1
0
peddlers-of-ketran/ARCHITECTURE.md

433 lines
13 KiB
Markdown

# 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`
```typescript
// 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`
```typescript
// 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:
```typescript
// ❌ 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:
```typescript
// 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
```typescript
// 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
```typescript
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:
```typescript
// 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:
```typescript
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:
```typescript
// 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:
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
- [MEDIACONTROL_API.md](./MEDIACONTROL_API.md) - WebRTC signaling protocol
- [server/routes/room/types.ts](./server/routes/room/types.ts) - Infrastructure types
- [server/routes/games/gameMetadata.ts](./server/routes/games/gameMetadata.ts) - Game metadata types
- [server/routes/games/gameAdapter.ts](./server/routes/games/gameAdapter.ts) - Compatibility adapter