GitHub

Networking & Retry

vuer-rtc tracks message acknowledgement status to enable retry on network failures.

How It Works

Every message in the journal has an ack field:

  • ack: false - Message hasn't been acknowledged by the server
  • ack: true - Server has confirmed receipt

When a message is committed locally, it starts with ack: false. When the server acknowledges it (via onServerAck), it becomes ack: true.

Retry Helpers

import {
  getUnackedMessages,
  hasPendingMessages,
  getPendingCount,
} from '@vuer-ai/vuer-rtc/client';

// Get all messages that need to be (re)sent
const messages = getUnackedMessages(state);

// Check if there's anything pending
if (hasPendingMessages(state)) {
  console.log(`${getPendingCount(state)} messages pending`);
}

Undo/Redo and Ack Reset

When you undo or redo an operation, two things happen:

  1. New undo/redo message is created with ack: false
  2. Target entry's ack is reset to false

This ensures that:

  • The undo/redo message itself needs to be sent
  • The target entry's deletedAt state change is tracked for sync
// Example: After undo
journal: [
  { msg: originalMsg, ack: false, deletedAt: 123456 },  // ack reset!
  { msg: undoMsg, ack: false },                          // new message
]

Implementing Retry

class RTCClient {
  private retryTimer: number | null = null;
  private retryDelay = 1000; // Start with 1 second
  private maxRetryDelay = 30000; // Max 30 seconds

  // Call when connection is restored
  async retryPendingMessages() {
    const messages = getUnackedMessages(this.store.getState());

    for (const msg of messages) {
      try {
        await this.sendToServer(msg);
      } catch (err) {
        // Schedule exponential backoff retry
        this.scheduleRetry();
        return;
      }
    }

    // All sent successfully, reset delay
    this.retryDelay = 1000;
  }

  private scheduleRetry() {
    if (this.retryTimer) return;

    this.retryTimer = setTimeout(() => {
      this.retryTimer = null;
      this.retryPendingMessages();
    }, this.retryDelay);

    // Exponential backoff
    this.retryDelay = Math.min(this.retryDelay * 2, this.maxRetryDelay);
  }

  // Call when receiving ack from server
  onAck(msgId: string) {
    this.store.dispatch(state => onServerAck(state, msgId));
  }
}

Server Implementation

The server should:

  1. Acknowledge each message after processing:

    ws.on('message', (data) => {
      const msg = JSON.parse(data);
    
      // Process the message...
    
      // Send ack back to client
      ws.send(JSON.stringify({ type: 'ack', msgId: msg.id }));
    });
  2. Handle duplicate messages gracefully (idempotent):

    const processedIds = new Set<string>();
    
    function processMessage(msg: CRDTMessage) {
      if (processedIds.has(msg.id)) {
        // Already processed, just send ack
        return;
      }
    
      processedIds.add(msg.id);
      // Process...
    }

Connection Status UI

You can track connection state and show sync status:

function useConnectionStatus(store: GraphStore) {
  const [status, setStatus] = useState<'connected' | 'reconnecting' | 'offline'>('connected');
  const pendingCount = getPendingCount(store.getState());

  return {
    status,
    pendingCount,
    isSynced: pendingCount === 0,
  };
}