# Troubleshooting & FAQ

Common issues and solutions when working with Vuer-RTC.

## Messages Not Syncing

**Problem**: Operations are committed but not appearing on other clients.

**Common causes**:

### WebSocket Connection Issues

1. **Check connection status**:
```typescript
const store = createGraph({
  sessionId: 'my-session',
  onSend: (msg) => {
    if (websocket.readyState !== WebSocket.OPEN) {
      console.error('WebSocket not open:', websocket.readyState);
      return;
    }
    websocket.send(JSON.stringify(msg));
  },
});
```

2. **Verify server is receiving messages**:
```typescript
// Server-side logging
wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    console.log('Received:', data);
  });
});
```

3. **Test with the echo server**:
```bash
# Run the test server
cd packages/vuer-rtc-server
pnpm start:echo
```

> **Note:** The echo server logs all messages to console. Use it to verify your client is sending properly formatted messages.

### Missing Acknowledgements

Messages need to be acknowledged for compaction and snapshot creation.

**Solution**: Ensure server echoes messages back to all clients:

```typescript
// Server-side: Echo to all clients in the room
wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    const msg = JSON.parse(data.toString());

    // Broadcast to all clients (including sender)
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify(msg));
      }
    });
  });
});
```

**Verify acknowledgements**:
```typescript
store.subscribe((state) => {
  const unacknowledged = state.journal.filter(entry => !entry.acked);
  if (unacknowledged.length > 10) {
    console.warn('Many unacknowledged messages:', unacknowledged.length);
  }
});
```

> **Note:** Messages remain unacknowledged until the server echoes them back. Without acks, snapshots cannot advance and the journal will grow indefinitely.

## Text CRDT Producing Wrong Results

**Problem**: Collaborative text editing produces unexpected or corrupted output.

**Root cause**: Causal ordering violations.

### Understanding the Issue

The Rope CRDT requires operations to be applied in causal order:

```typescript
// Client A inserts at position 5
{ clock: { A: 1 }, ops: [{ ot: 'rope.insert', index: 5, text: 'hello' }] }

// Client B deletes from position 3 (depends on A's insert)
{ clock: { A: 1, B: 1 }, ops: [{ ot: 'rope.delete', index: 3, length: 2 }] }
```

Client B's operation has `clock: { A: 1, B: 1 }`, meaning it depends on A's operation (`A: 1`).

### Solution: Buffer Out-of-Order Messages

```typescript
const pendingMessages: CRDTMessage[] = [];

function receive(msg: CRDTMessage) {
  // Check if we have all dependencies
  if (!canApply(msg)) {
    pendingMessages.push(msg);
    return;
  }

  store.receive(msg);

  // Try to apply pending messages
  let applied = true;
  while (applied) {
    applied = false;
    for (let i = 0; i < pendingMessages.length; i++) {
      if (canApply(pendingMessages[i])) {
        store.receive(pendingMessages[i]);
        pendingMessages.splice(i, 1);
        applied = true;
        break;
      }
    }
  }
}

function canApply(msg: CRDTMessage): boolean {
  const currentClock = store.getState().clock;
  for (const [sessionId, time] of Object.entries(msg.clock)) {
    if ((currentClock[sessionId] || 0) < time - 1) {
      return false; // Missing dependency
    }
  }
  return true;
}
```

> **Note:** If you see text appearing in the wrong positions or deletions affecting the wrong characters, check message ordering. The vector clock in each message tracks dependencies.

### Quick Test

```typescript
// Log clocks to verify ordering
store.subscribe((state) => {
  console.log('Current clock:', state.clock);
});

websocket.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  console.log('Received msg clock:', msg.clock);
  store.receive(msg);
};
```

## Undo Not Working

**Problem**: `store.undo()` returns `null` or has no effect.

**Common causes**:

### 1. No Committed Messages to Undo

```typescript
store.edit({ ot: 'node.insert', key: 'scene', path: 'children', value: { key: 'cube' } });
const result = store.undo(); // null - nothing committed yet

store.commit('Add cube');
const result = store.undo(); // Works now
```

> **Note:** Only committed messages can be undone. Operations in the edit buffer are not yet in the journal.

### 2. Target Message Already Compacted

When a snapshot is created, older journal entries are compacted:

```typescript
// Message was committed at index 10
store.commit('Add cube');

// After compaction, snapshot includes up to index 50
// Message at index 10 is removed from journal

store.undo(); // null - message no longer in journal
```

**Solution**: Adjust compaction threshold:

```typescript
const store = createGraph({
  sessionId: 'my-session',
  onSend: (msg) => websocket.send(msg),
  compactionThreshold: 1000, // Keep more history (default: 100)
});
```

> **Note:** Higher compaction thresholds mean more memory usage. Balance undo history depth with memory constraints.

### 3. Message from Different Session

`undo()` only undoes messages from the current session:

```typescript
// This session is 'session-A'
const store = createGraph({ sessionId: 'session-A', ... });

// Message from 'session-B' is received
store.receive({
  id: 'msg-123',
  sessionId: 'session-B', // Different session
  ops: [...]
});

store.undo(); // Won't undo session-B's message
```

To undo a specific message:

```typescript
// Undo a specific message ID
store.edit({
  ot: 'meta.undo',
  targetMsgId: 'msg-123',
});
store.commit('Undo remote change');
```

## Performance Degrading Over Time

**Problem**: Application becomes slower after extended use.

**Root cause**: Journal grows too large without compaction.

### Diagnosis

```typescript
store.subscribe((state) => {
  console.log('Journal size:', state.journal.length);
  console.log('Snapshot coverage:', state.snapshot.index);
});
```

If the journal has hundreds or thousands of entries, you need more frequent compaction.

### Solution 1: Adjust Compaction Threshold

```typescript
const store = createGraph({
  sessionId: 'my-session',
  onSend: (msg) => websocket.send(msg),
  compactionThreshold: 50, // Compact more frequently (default: 100)
});
```

### Solution 2: Manual Compaction

```typescript
// Force compaction after major operations
store.commit('Major change');
const state = store.getState();
if (state.journal.length > 200) {
  // Compaction happens automatically on next receive
  // when acked messages exceed threshold
}
```

### Solution 3: Periodic Cleanup

```typescript
setInterval(() => {
  const state = store.getState();
  const ackedCount = state.journal.filter(e => e.acked).length;

  if (ackedCount > 150) {
    // Trigger compaction by receiving a dummy message
    // (compaction runs during receive if threshold met)
  }
}, 60000); // Check every minute
```

> **Note:** Compaction bakes acknowledged journal entries into the snapshot and removes them from the journal. This keeps replay fast.

## Type Errors in TypeScript

**Problem**: Import errors or missing types.

### Incorrect Import Path

```typescript
// ❌ Wrong - tries to import from package root

// Error: Module '"@vuer-ai/vuer-rtc"' has no exported member 'Rope'

// ✅ Correct - use subpath export

```

### Available Subpath Exports

```typescript
// Core CRDT operations

// React hooks

// Text CRDT

// Server utilities

```

> **Note:** Check `package.json` exports field for the complete list of available subpath exports.

### Missing Type Definitions

If types are not resolving:

```bash
# Ensure TypeScript can find the package
pnpm install
pnpm exec tsc --traceResolution | grep vuer-rtc
```

Check your `tsconfig.json`:

```json
{
  "compilerOptions": {
    "moduleResolution": "bundler",  // or "node16"
    "resolvePackageJsonExports": true
  }
}
```

## State Divergence Between Clients

**Problem**: Clients show different states after the same operations.

**Root cause**: Clock synchronization issues or non-deterministic conflict resolution.

### Verify Message Order

All clients must apply messages in the same causal order:

```typescript
// Log applied messages
store.subscribe((state) => {
  console.log('Journal:', state.journal.map(e => ({
    id: e.msg.id,
    sessionId: e.msg.sessionId,
    clock: e.msg.clock,
  })));
});
```

Compare logs across clients - the journal order should be identical.

### Check Last-Write-Wins Ordering

For `*.set` operations, `lamportTime` determines the winner:

```typescript
// Client A (lamportTime: 100)
{ ops: [{ ot: 'scalar.set', key: 'cube', path: 'visible', value: true }] }

// Client B (lamportTime: 101) - wins
{ ops: [{ ot: 'scalar.set', key: 'cube', path: 'visible', value: false }] }
```

**Solution**: Ensure lamport time increments correctly:

```typescript
const store = createGraph({
  sessionId: 'my-session',
  onSend: (msg) => {
    console.log('Sending lamport:', msg.lamportTime);
    websocket.send(msg);
  },
});
```

> **Note:** If lamport times are not monotonically increasing, conflict resolution will be inconsistent. Verify the server is not modifying lamport times.

### Debugging Tool: State Hash

Create a deterministic hash of state for comparison:

```typescript

function stateHash(store: GraphStore): string {
  const state = store.getState();
  const normalized = JSON.stringify({
    graph: state.graph,
    clock: state.clock,
  });
  return createHash('sha256').update(normalized).digest('hex');
}

// Log periodically
setInterval(() => {
  console.log('State hash:', stateHash(store));
}, 5000);
```

All clients should show the same hash after convergence.

## Memory Leaks

**Problem**: Memory usage grows unbounded over time.

**Root cause**: Journal not being compacted.

### Diagnosis

```typescript
// Monitor memory usage
setInterval(() => {
  const state = store.getState();
  console.log({
    journalSize: state.journal.length,
    editBufferSize: Object.keys(state.edits).length,
    graphNodes: Object.keys(state.graph.children).length,
  });
}, 30000);
```

### Common Causes

1. **Messages never acknowledged**:
```typescript
// Check ack status
const unacked = store.getState().journal.filter(e => !e.acked);
console.log('Unacked messages:', unacked.length);
```

Solution: Ensure server echoes messages back.

2. **Compaction disabled or threshold too high**:
```typescript
const store = createGraph({
  sessionId: 'my-session',
  onSend: (msg) => websocket.send(msg),
  compactionThreshold: 50, // Lower threshold
});
```

3. **Edits never committed**:
```typescript
// Edit buffer holding uncommitted operations
store.edit({ ... });
store.edit({ ... });
// Forgot to call commit()

// Solution: Always commit edits
store.commit('Changes');
```

> **Note:** Uncommitted edits stay in memory indefinitely. Make sure to call `commit()` after editing operations, especially in event handlers like `onMouseUp` or `onDragEnd`.

## WebSocket Disconnections

**Problem**: Connection drops and messages are lost.

### Implement Retry Logic

```typescript

function connectWithRetry(url: string): Promise {
  return retryWithBackoff(
    () => {
      return new Promise((resolve, reject) => {
        const ws = new WebSocket(url);
        ws.onopen = () => resolve(ws);
        ws.onerror = (err) => reject(err);
      });
    },
    {
      maxRetries: 5,
      baseDelay: 1000,
      maxDelay: 30000,
      backoffFactor: 2,
    }
  );
}

// Usage
const ws = await connectWithRetry('ws://localhost:8080');
```

### Reconnection with Message Replay

Keep a buffer of sent messages for replay:

```typescript
const sentMessages: CRDTMessage[] = [];

const store = createGraph({
  sessionId: 'my-session',
  onSend: (msg) => {
    sentMessages.push(msg);
    websocket.send(JSON.stringify(msg));
  },
});

websocket.onclose = async () => {
  console.log('Disconnected, reconnecting...');
  const newWs = await connectWithRetry('ws://localhost:8080');

  // Replay unacknowledged messages
  const unacked = sentMessages.filter(msg => {
    const entry = store.getState().journal.find(e => e.msg.id === msg.id);
    return entry && !entry.acked;
  });

  unacked.forEach(msg => {
    newWs.send(JSON.stringify(msg));
  });
};
```

> **Note:** The retry utilities in `@vuer-ai/vuer-rtc/networking` provide exponential backoff and jitter to avoid thundering herd problems.

### Heartbeat to Detect Disconnections

```typescript
let heartbeatInterval: NodeJS.Timeout;

websocket.onopen = () => {
  // Send ping every 30 seconds
  heartbeatInterval = setInterval(() => {
    websocket.send(JSON.stringify({ type: 'ping' }));
  }, 30000);
};

websocket.onclose = () => {
  clearInterval(heartbeatInterval);
};

websocket.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === 'pong') {
    // Connection alive
    return;
  }
  store.receive(msg);
};
```

## Still Having Issues?

> **Note:** Open an issue on [GitHub](https://github.com/vuer-ai/vuer-rtc/issues) with:
>   - Minimal reproduction
>   - Client and server code
>   - Console logs showing the issue
>   - Expected vs actual behavior

## Next Steps

<div className="flex flex-wrap gap-4 not-prose">
  <a
    href="/architecture"
    className="inline-flex items-center px-4 py-2 bg-blue-600 dark:bg-blue-500 text-white rounded-md hover:bg-blue-700 dark:hover:bg-blue-600"
  >
    Architecture
  </a>
  <a
    href="/networking"
    className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-900 dark:text-gray-100"
  >
    Networking & Retry
  </a>
  <a
    href="/examples"
    className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-900 dark:text-gray-100"
  >
    Examples
  </a>
  <a
    href="/development"
    className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-300 rounded-md hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-900 dark:text-gray-100"
  >
    Development
  </a>
</div>
