Skip to content

Real-time Collaboration Example

This example demonstrates building a collaborative document editor with Bindra's real-time features.

Features Demonstrated

  • ✅ WebSocket real-time synchronization
  • ✅ Optimistic updates for instant UI feedback
  • ✅ Auto-save with debounce and max wait
  • ✅ Presence awareness (who's editing)
  • ✅ Conflict detection and resolution
  • ✅ Connection status handling
  • ✅ XSS protection with field sanitization

Architecture

┌─────────────┐         WebSocket          ┌─────────────┐
│   Client A  │◄───────────────────────────►│   Server    │
└─────────────┘                             └─────────────┘


┌─────────────┐         WebSocket                  │
│   Client B  │◄───────────────────────────────────┘
└─────────────┘

Setup

1. Install Dependencies

bash
pnpm install

2. Start WebSocket Server

You'll need a WebSocket server that broadcasts document updates. Example using Node.js:

javascript
// server.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

const clients = new Set();

wss.on('connection', (ws) => {
  clients.add(ws);
  
  ws.on('message', (message) => {
    // Broadcast to all clients
    clients.forEach(client => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(message);
      }
    });
  });
  
  ws.on('close', () => {
    clients.delete(ws);
  });
});

3. Run the Example

bash
npx ts-node index.ts

Key Concepts

Real-time Configuration

typescript
const documents = new DataSource<Document>({
  url: 'https://api.example.com/documents',
  realtime: {
    enabled: true,
    url: 'wss://api.example.com/ws',
    reconnect: true,
    reconnectInterval: 5000
  },
  optimisticUpdates: true
});

Event Handling

typescript
// Listen for remote changes
documents.on('created', (doc) => {
  console.log('New document:', doc);
});

documents.on('updated', (doc) => {
  if (doc.lastEditedBy !== currentUser) {
    // Handle conflict
  }
});

// Connection status
documents.on('ws:connected', () => {
  updateUI('connected');
});

Auto-save with Debounce

typescript
import { debounceWithMaxWait } from 'bindra';

const autoSave = debounceWithMaxWait(
  (id, changes) => documents.update(id, changes),
  2000,   // Wait 2s after last edit
  10000   // But save at least every 10s
);

editor.on('change', () => {
  autoSave(docId, { content: editor.getContent() });
});

Optimistic Updates

typescript
// With optimisticUpdates: true
await documents.update(id, { content: 'New text' });
// UI updates immediately (optimistic)
// If server fails, automatically rolls back

Presence Broadcasting

typescript
// Send presence through WebSocket
documents.sendWebSocketMessage({
  type: 'presence',
  data: {
    userId: currentUser.id,
    cursorPosition: 42,
    color: '#3b82f6'
  }
});

// Receive presence from others
documents.on('realtime:message', (message) => {
  if (message.type === 'presence') {
    showUserCursor(message.data);
  }
});

Conflict Resolution Strategies

1. Last Write Wins (Simple)

typescript
if (remoteDoc.version > localDoc.version) {
  // Accept remote changes
  return remoteDoc;
}

2. Operational Transformation (Advanced)

typescript
function resolveConflict(local, remote) {
  // Apply OT algorithm to merge changes
  return mergeOperations(local.ops, remote.ops);
}

3. Manual Resolution

typescript
// Show conflict dialog to user
showConflictDialog({
  local: localDoc,
  remote: remoteDoc,
  onResolve: (resolved) => {
    documents.update(id, resolved);
  }
});

Best Practices

  1. Always handle connection errors

    typescript
    documents.on('ws:error', handleError);
    documents.on('ws:disconnected', showOfflineMode);
  2. Use version numbers or timestamps

    typescript
    interface Document {
      version: number;
      lastEditedAt: Date;
    }
  3. Sanitize user input

    typescript
    security: {
      sanitizeFields: ['title', 'content']
    }
  4. Implement proper debouncing

    typescript
    // Don't save on every keystroke
    debounceWithMaxWait(save, 1000, 10000);
  5. Clean up on unmount

    typescript
    onUnmount(() => {
      documents.disconnectWebSocket();
      documents.off('updated', handler);
    });

Learn More

Released under the MIT License.