1
0
peddlers-of-ketran/MIGRATION_EXAMPLE.md

13 KiB

Migration Example: Using the Pluggable Architecture

This document provides concrete examples of how to use the new pluggable architecture, demonstrating both backward compatibility and new patterns.

Table of Contents

  1. Using the Adapter for Backward Compatibility
  2. Writing New Code with Metadata Pattern
  3. Creating a New Application Type
  4. Migration Path

Using the Adapter for Backward Compatibility

The adapter layer allows existing code to work unchanged while the codebase gradually migrates to the new architecture.

Example: Existing Code Continues to Work

import { wrapGame, wrapSession } from './routes/games/gameAdapter';
import type { Game } from './routes/games/types';

// Load existing game from database/storage
const loadedGame: Game = loadGameFromDatabase(gameId);

// Wrap the game object to enable backward compatibility
const game = wrapGame(loadedGame);

// Old code continues to work unchanged!
game.turn = { color: "red", roll: 7 };           // Proxied to game.metadata.turn
game.players["red"].resources = 5;               // Proxied to game.metadata.players["red"]

// Session operations also work
const session = game.sessions["session123"];
session.color = "blue";                          // Proxied to session.metadata.color
session.player = game.players["blue"];           // Proxied to session.metadata.player

console.log("Player color:", session.color);     // Reads from session.metadata.color
console.log("Turn data:", game.turn);            // Reads from game.metadata.turn

How the Proxy Works

The adapter uses JavaScript Proxy to intercept property access:

// Reading game-specific properties
const color = session.color;
// Behind the scenes: session.metadata?.color

// Writing game-specific properties
session.color = "red";
// Behind the scenes: session.metadata = { ...session.metadata, color: "red" }

// Infrastructure properties work normally
const sessionId = session.id;          // Direct access (not proxied)
const isLive = session.live;           // Direct access (not proxied)

Writing New Code with Metadata Pattern

New code should use the metadata pattern directly for clean separation of concerns.

Example: Session Color Selection (New Pattern)

import type { GameSession, GameSessionMetadata } from './routes/games/gameMetadata';
import { updateSessionMetadata, getSessionMetadata } from './routes/room/helpers';

function handleColorSelection(session: GameSession, color: string): void {
  // Update game-specific metadata
  updateSessionMetadata<GameSessionMetadata>(session, {
    color: color
  });

  // Infrastructure fields remain separate
  session.live = true;
  session.lastActive = Date.now();
}

function getPlayerColor(session: GameSession): string | undefined {
  const metadata = getSessionMetadata<GameSessionMetadata>(session);
  return metadata?.color;
}

Example: Getting Participants with Game Data

import { getParticipants } from './routes/room/helpers';
import type { GameRoom, GameSessionMetadata } from './routes/games/gameMetadata';

function getGameParticipants(room: GameRoom) {
  // Get base participant data (reusable)
  const baseParticipants = getParticipants(room.sessions);

  // Extend with game-specific data
  return baseParticipants.map(p => {
    const session = room.sessions[p.session_id];
    const metadata = session.metadata as GameSessionMetadata | undefined;

    return {
      ...p,                           // Base participant data
      color: metadata?.color || null, // Game-specific data
    };
  });
}

Example: Room State Management

import type { GameRoom, GameRoomMetadata } from './routes/games/gameMetadata';
import { updateRoomMetadata, getRoomMetadata } from './routes/room/helpers';

function initializeGameRoom(room: GameRoom): void {
  // Initialize game-specific metadata
  updateRoomMetadata<GameRoomMetadata>(room, {
    players: {},
    turn: { color: undefined, actions: [] },
    placements: { corners: [], roads: [] },
    chat: [],
    developmentCards: [],
  });

  // Infrastructure state remains separate
  room.state = "lobby";
}

function updateTurn(room: GameRoom, color: string, roll: number): void {
  const metadata = getRoomMetadata<GameRoomMetadata>(room);
  if (!metadata) return;

  // Update game state in metadata
  metadata.turn = {
    ...metadata.turn,
    color,
    roll,
  };

  // Infrastructure state changes separately
  room.state = "playing";
}

Creating a New Application Type

The infrastructure can be reused for any multi-user WebRTC application.

Example: Creating a Whiteboard Application

// Step 1: Define your metadata types
// File: server/routes/whiteboard/whiteboardMetadata.ts

import type { Session, Room } from '../room/types';
import type { Player } from './whiteboardTypes';

export interface WhiteboardSessionMetadata {
  tool: "pen" | "eraser" | "shape";
  color: string;
  brushSize: number;
  selectedObject?: string;
}

export interface WhiteboardRoomMetadata {
  canvas: CanvasObject[];
  history: HistoryEntry[];
  locked: boolean;
}

export type WhiteboardSession = Session<WhiteboardSessionMetadata>;
export type WhiteboardRoom = Room<WhiteboardRoomMetadata>;
// Step 2: Use the reusable infrastructure
// File: server/routes/whiteboard/handlers.ts

import type { WhiteboardRoom, WhiteboardSession, WhiteboardSessionMetadata } from './whiteboardMetadata';
import {
  getParticipants,
  updateSessionMetadata,
  updateRoomMetadata
} from '../room/helpers';

export function handleToolChange(
  session: WhiteboardSession,
  tool: "pen" | "eraser" | "shape"
): void {
  updateSessionMetadata<WhiteboardSessionMetadata>(session, { tool });
}

export function getWhiteboardParticipants(room: WhiteboardRoom) {
  const baseParticipants = getParticipants(room.sessions);

  // Add whiteboard-specific data
  return baseParticipants.map(p => {
    const session = room.sessions[p.session_id];
    const metadata = session.metadata;

    return {
      ...p,
      tool: metadata?.tool || "pen",
      color: metadata?.color || "#000000",
      brushSize: metadata?.brushSize || 5,
    };
  });
}

export function addCanvasObject(room: WhiteboardRoom, object: CanvasObject): void {
  if (!room.metadata) {
    updateRoomMetadata(room, {
      canvas: [],
      history: [],
      locked: false,
    });
  }

  room.metadata!.canvas.push(object);
  room.metadata!.history.push({
    type: 'add',
    timestamp: Date.now(),
    object,
  });
}
// Step 3: Reuse MediaControl component (client-side)
// File: client/src/WhiteboardApp.tsx

import { MediaAgent, MediaControl } from './MediaControl';
import type { Session } from './GlobalContext';

function WhiteboardApp() {
  const [session, setSession] = useState<Session | null>(null);
  const [peers, setPeers] = useState<Record<string, Peer>>({});

  return (
    <>
      {/* Reuse MediaAgent - no changes needed! */}
      <MediaAgent
        session={session}
        socketUrl={socketUrl}
        peers={peers}
        setPeers={setPeers}
      />

      {/* Reuse MediaControl - no changes needed! */}
      {participants.map(p => (
        <MediaControl
          key={p.session_id}
          peer={peers[p.session_id]}
          isSelf={p.session_id === session?.id}
          sendJsonMessage={sendJsonMessage}
        />
      ))}

      {/* Your whiteboard-specific UI */}
      <Canvas participants={participants} />
    </>
  );
}

Migration Path

Phase 1: Add Metadata Layer ( Complete)

The infrastructure and metadata types are now defined. Code can use either pattern:

// Old pattern (via adapter)
session.color = "red";

// New pattern (direct metadata access)
updateSessionMetadata(session, { color: "red" });

Phase 2: Migrate Functions One by One

Gradually update functions to use metadata pattern:

// Before: Mixed concerns
function setPlayerColor(session: any, color: string): void {
  session.color = color;  // Direct property access
  session.live = true;
}

// After: Separated concerns
function setPlayerColor(session: GameSession, color: string): void {
  updateSessionMetadata<GameSessionMetadata>(session, { color });  // Metadata
  session.live = true;  // Infrastructure
}

Phase 3: Update Message Handlers

Update WebSocket message handlers to work with metadata:

// Before
case "set": {
  if (data.field === "color") {
    session.color = data.value;
  }
  break;
}

// After
case "set": {
  if (data.field === "color") {
    updateSessionMetadata<GameSessionMetadata>(session, {
      color: data.value
    });
  }
  break;
}

Phase 4: Remove Adapters (Future)

Once all code is migrated, remove the adapter layer and use pure metadata:

// Pure metadata architecture (no adapters)
const session: GameSession = {
  // Infrastructure fields
  id: "abc123",
  name: "Alice",
  live: true,
  has_media: true,
  lastActive: Date.now(),

  // Application-specific data
  metadata: {
    color: "red",
    player: {...},
    resources: 5,
  }
};

Benefits of the New Architecture

1. Reusability

The same infrastructure works for different applications:

// Game application
const gameRoom: Room<GameRoomMetadata> = {...};

// Whiteboard application
const whiteboardRoom: Room<WhiteboardRoomMetadata> = {...};

// Chat application
const chatRoom: Room<ChatRoomMetadata> = {...};

// All use the same getParticipants, session management, WebRTC signaling!

2. Type Safety

TypeScript ensures correct metadata usage:

const session: GameSession = {...};

// ✅ Type-safe
updateSessionMetadata<GameSessionMetadata>(session, { color: "red" });

// ❌ TypeScript error: 'invalidField' doesn't exist
updateSessionMetadata<GameSessionMetadata>(session, { invalidField: "value" });

3. Clear Separation

Infrastructure and application logic are clearly separated:

// Infrastructure (works for any app)
import { getParticipants, updateSessionActivity } from './room/helpers';

// Application-specific (game logic)
import { GameSession, GameRoom } from './games/gameMetadata';
import { calculatePoints, updateTurn } from './games/gameLogic';

4. Easy Testing

Each layer can be tested independently:

// Test infrastructure
describe('getParticipants', () => {
  it('returns participant list from any session type', () => {
    const sessions = { /* any session structure */ };
    const participants = getParticipants(sessions);
    expect(participants).toHaveLength(2);
  });
});

// Test game logic
describe('calculatePoints', () => {
  it('calculates game points from metadata', () => {
    const room: GameRoom = { /* ... */ };
    calculatePoints(room);
    expect(room.metadata?.players['red'].points).toBe(10);
  });
});

Common Patterns

Pattern 1: Extending Participant Lists

function getExtendedParticipants<TMetadata extends Record<string, any>>(
  room: Room<any>,
  extender: (session: Session<any>) => TMetadata
) {
  const baseParticipants = getParticipants(room.sessions);

  return baseParticipants.map(p => ({
    ...p,
    ...extender(room.sessions[p.session_id])
  }));
}

// Usage
const gameParticipants = getExtendedParticipants(gameRoom, (session) => ({
  color: session.metadata?.color || null,
  points: session.metadata?.player?.points || 0,
}));

Pattern 2: Metadata Initialization

function ensureSessionMetadata<TMetadata>(
  session: Session<TMetadata>,
  defaults: TMetadata
): void {
  if (!session.metadata) {
    session.metadata = defaults;
  }
}

// Usage
ensureSessionMetadata<GameSessionMetadata>(session, {
  color: undefined,
  player: undefined,
  resources: 0,
});

Pattern 3: Safe Metadata Access

function getMetadataField<TMetadata, K extends keyof TMetadata>(
  session: Session<TMetadata>,
  field: K,
  defaultValue: TMetadata[K]
): TMetadata[K] {
  return session.metadata?.[field] ?? defaultValue;
}

// Usage
const color = getMetadataField<GameSessionMetadata, 'color'>(
  session,
  'color',
  'gray'
);

Summary

The new architecture provides:

  • Clean separation between infrastructure and application logic
  • Reusable components (MediaControl, Room helpers, WebRTC signaling)
  • Type safety with generic TypeScript types
  • Backward compatibility via adapter pattern
  • Extensibility for new application types

By following these patterns, you can create new applications that leverage the existing WebRTC and Room infrastructure without modification.