Real-Time Collaboration App

Scenario: Building Figma-like Collaboration

You’re building a real-time collaborative design tool with:

  • WebSocket connections for live updates
  • Canvas rendering with WebGL
  • Conflict resolution for concurrent edits
  • Presence awareness
  • Offline support with sync

CLAUDE.md for Real-Time Features

## Real-Time Collaboration Architecture
 
### Core Types
```typescript
// Collaboration Types
interface CollaborationSession {
  id: string;
  documentId: string;
  participants: Map<string, Participant>;
  operations: OperationalTransform[];
  version: number;
}
 
interface Participant {
  id: string;
  user: User;
  cursor: CursorPosition;
  selection: Selection | null;
  color: string;
  lastSeen: number;
  permissions: Permission[];
}
 
interface CursorPosition {
  x: number;
  y: number;
  viewport: Viewport;
}
 
// Operational Transform Types
type Operation = 
  | InsertOperation
  | DeleteOperation
  | MoveOperation
  | StyleOperation
  | GroupOperation;
 
interface InsertOperation {
  type: 'insert';
  id: string;
  parentId: string;
  index: number;
  element: DesignElement;
  timestamp: number;
  userId: string;
}
 
interface DeleteOperation {
  type: 'delete';
  id: string;
  timestamp: number;
  userId: string;
}
 
// Conflict Resolution
interface ConflictResolver {
  resolve(
    local: Operation,
    remote: Operation,
    baseVersion: number
  ): ResolvedOperation[];
}
 
// WebSocket Message Types
type WSMessage = 
  | { type: 'operation'; operation: Operation; version: number }
  | { type: 'cursor'; position: CursorPosition; userId: string }
  | { type: 'selection'; selection: Selection; userId: string }
  | { type: 'presence'; status: 'join' | 'leave'; participant: Participant }
  | { type: 'sync'; operations: Operation[]; version: number };

WebSocket Client Implementation

export class CollaborationClient {
  private ws: WebSocket | null = null;
  private messageQueue: WSMessage[] = [];
  private reconnectTimer: NodeJS.Timeout | null = null;
  private eventHandlers = new Map<string, Set<Function>>();
  
  constructor(
    private url: string,
    private documentId: string,
    private userId: string
  ) {}
  
  connect(): Promise<void> {
    return new Promise((resolve, reject) => {
      this.ws = new WebSocket(this.url);
      
      this.ws.onopen = () => {
        this.flushMessageQueue();
        this.emit('connected');
        resolve();
      };
      
      this.ws.onmessage = (event) => {
        const message = JSON.parse(event.data) as WSMessage;
        this.handleMessage(message);
      };
      
      this.ws.onerror = (error) => {
        this.emit('error', error);
        reject(error);
      };
      
      this.ws.onclose = () => {
        this.emit('disconnected');
        this.scheduleReconnect();
      };
    });
  }
  
  private handleMessage(message: WSMessage): void {
    switch (message.type) {
      case 'operation':
        this.handleOperation(message.operation, message.version);
        break;
      case 'cursor':
        this.emit('cursor', { userId: message.userId, position: message.position });
        break;
      case 'selection':
        this.emit('selection', { userId: message.userId, selection: message.selection });
        break;
      case 'presence':
        this.emit('presence', message);
        break;
      case 'sync':
        this.handleSync(message.operations, message.version);
        break;
    }
  }
  
  sendOperation(operation: Operation): void {
    const message: WSMessage = {
      type: 'operation',
      operation,
      version: this.currentVersion,
    };
    
    this.send(message);
  }
  
  private send(message: WSMessage): void {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(message));
    } else {
      this.messageQueue.push(message);
    }
  }
  
  private flushMessageQueue(): void {
    while (this.messageQueue.length > 0 && this.ws?.readyState === WebSocket.OPEN) {
      const message = this.messageQueue.shift()!;
      this.ws.send(JSON.stringify(message));
    }
  }
  
  on<T extends keyof CollaborationEvents>(
    event: T,
    handler: CollaborationEvents[T]
  ): void {
    if (!this.eventHandlers.has(event)) {
      this.eventHandlers.set(event, new Set());
    }
    this.eventHandlers.get(event)!.add(handler);
  }
  
  off<T extends keyof CollaborationEvents>(
    event: T,
    handler: CollaborationEvents[T]
  ): void {
    this.eventHandlers.get(event)?.delete(handler);
  }
  
  private emit(event: string, ...args: any[]): void {
    this.eventHandlers.get(event)?.forEach(handler => handler(...args));
  }
}
 
// React Hook for Collaboration
export function useCollaboration(documentId: string) {
  const [participants, setParticipants] = useState<Map<string, Participant>>(new Map());
  const [connected, setConnected] = useState(false);
  const clientRef = useRef<CollaborationClient>();
  
  useEffect(() => {
    const client = new CollaborationClient(
      process.env.NEXT_PUBLIC_WS_URL!,
      documentId,
      getCurrentUserId()
    );
    
    clientRef.current = client;
    
    client.on('connected', () => setConnected(true));
    client.on('disconnected', () => setConnected(false));
    
    client.on('presence', ({ status, participant }) => {
      setParticipants(prev => {
        const next = new Map(prev);
        if (status === 'join') {
          next.set(participant.id, participant);
        } else {
          next.delete(participant.id);
        }
        return next;
      });
    });
    
    client.on('cursor', ({ userId, position }) => {
      setParticipants(prev => {
        const next = new Map(prev);
        const participant = next.get(userId);
        if (participant) {
          next.set(userId, { ...participant, cursor: position });
        }
        return next;
      });
    });
    
    client.connect();
    
    return () => {
      client.disconnect();
    };
  }, [documentId]);
  
  return {
    participants,
    connected,
    sendOperation: (op: Operation) => clientRef.current?.sendOperation(op),
    updateCursor: (pos: CursorPosition) => clientRef.current?.updateCursor(pos),
  };
}

Offline Support with IndexedDB

// Offline storage for operations
export class OfflineStore {
  private db: IDBDatabase | null = null;
  
  async initialize(): Promise<void> {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('collaboration', 1);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve();
      };
      
      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        
        if (!db.objectStoreNames.contains('operations')) {
          const store = db.createObjectStore('operations', { 
            keyPath: 'id',
            autoIncrement: true 
          });
          store.createIndex('documentId', 'documentId', { unique: false });
          store.createIndex('timestamp', 'timestamp', { unique: false });
        }
        
        if (!db.objectStoreNames.contains('documents')) {
          db.createObjectStore('documents', { keyPath: 'id' });
        }
      };
    });
  }
  
  async saveOperation(
    documentId: string,
    operation: Operation
  ): Promise<void> {
    const tx = this.db!.transaction(['operations'], 'readwrite');
    const store = tx.objectStore('operations');
    
    await store.add({
      documentId,
      operation,
      timestamp: Date.now(),
      synced: false,
    });
  }
  
  async getUnsyncedOperations(
    documentId: string
  ): Promise<Operation[]> {
    const tx = this.db!.transaction(['operations'], 'readonly');
    const store = tx.objectStore('operations');
    const index = store.index('documentId');
    
    const operations: Operation[] = [];
    
    return new Promise((resolve, reject) => {
      const request = index.openCursor(IDBKeyRange.only(documentId));
      
      request.onsuccess = () => {
        const cursor = request.result;
        if (cursor) {
          if (!cursor.value.synced) {
            operations.push(cursor.value.operation);
          }
          cursor.continue();
        } else {
          resolve(operations);
        }
      };
      
      request.onerror = () => reject(request.error);
    });
  }
  
  async markOperationsSynced(
    documentId: string,
    operationIds: string[]
  ): Promise<void> {
    const tx = this.db!.transaction(['operations'], 'readwrite');
    const store = tx.objectStore('operations');
    
    for (const id of operationIds) {
      const request = store.get(id);
      request.onsuccess = () => {
        const record = request.result;
        if (record) {
          record.synced = true;
          store.put(record);
        }
      };
    }
  }
}

## Key Implementation Patterns

1. **Event-Driven Architecture**: Use WebSocket events for all real-time updates
2. **Operational Transforms**: Ensure consistency with concurrent edits
3. **Optimistic UI**: Update locally first, then sync
4. **Presence System**: Show who's working where
5. **Offline Queue**: Store operations when disconnected

## Performance Considerations

- Use WebSocket message batching
- Implement cursor position throttling
- Cache frequently accessed data
- Use WebWorkers for heavy computations
- Implement virtual scrolling for large canvases

## Related Resources

- [[../interactive-typescript|Interactive TypeScript Guide]]
- [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
- [Operational Transformation](http://ot.substance.io/visualization/)

## 🧭 Quick Navigation

[← E-Commerce](e-commerce-migration) | [Back to Scenarios](../index) | [Financial Trading →](/docs/reference/templates/real-world-scenarios/financial-trading-platform)