Vuer RTC

Text Document API - Client Store

The Text Document API provides a high-level client-side store for managing collaborative text documents, similar to the createGraph API for scene graphs. It's built on top of the TextRope CRDT and handles all the complexities of state management, operation journaling, undo/redo, and coalescing.

Features

  • Simple API - insert(), delete(), replace() operations with position-based editing
  • State Management - Automatic state tracking with subscription support
  • Operation Journaling - All operations are journaled with vector clocks and Lamport timestamps
  • Undo/Redo - Built-in undo/redo with proper CRDT semantics
  • Coalescing - Automatic operation batching for better performance
  • Server Sync - Ready for WebSocket integration with onSend callback

Quick Start

typescript
import { createTextDocument } from '@vuer-ai/vuer-rtc';

// Create a text document store
const doc = createTextDocument({
  sessionId: 'alice-123',
  onSend: (msg) => {
    // Send message to server via WebSocket
    websocket.send(JSON.stringify(msg));
  },
});

// Insert text
doc.insert(0, 'Hello World');
doc.commit('Initial text');

// Edit text
doc.delete(6, 5);  // Delete "World"
doc.insert(6, 'Alice');
doc.commit('Replace name');

// Get current text
console.log(doc.getText());  // "Hello Alice"

// Undo last change
doc.undo();
console.log(doc.getText());  // "Hello World"

// Redo
doc.redo();
console.log(doc.getText());  // "Hello Alice"

API Reference

Factory Functions

createTextDocument(options)

Creates a new text document store.

Parameters:

  • options.sessionId (string, required) - Unique session identifier
  • options.initialSnapshot (TextSnapshot, optional) - Initialize from server snapshot
  • options.onSend (function, optional) - Callback when messages are ready to send
  • options.onStateChange (function, optional) - Callback when state changes
  • options.onMessageSent (function, optional) - Callback after each message sent
  • options.coalescingEnabled (boolean, optional) - Enable automatic commit coalescing (default: false)
  • options.coalescingDelayMs (number, optional) - Coalescing delay in milliseconds (default: 300)

Returns: TextDocumentStore

typescript
const doc = createTextDocument({
  sessionId: 'user-123',
  coalescingEnabled: true,
  coalescingDelayMs: 500,
  onSend: (msg) => ws.send(msg),
  onStateChange: (state) => console.log('State changed:', state),
});

createTextDocumentFromServer(options)

Creates a text document store initialized from server state.

Parameters: Same as createTextDocument plus:

  • options.snapshot (TextSnapshot, required) - Server snapshot
  • options.journal (TextMessage[], required) - Journal entries
typescript
const doc = createTextDocumentFromServer({
  sessionId: 'user-123',
  snapshot: serverSnapshot,
  journal: serverJournal,
  onSend: (msg) => ws.send(msg),
});

Store Methods

Editing Operations

insert(position, text)

Insert text at the specified position.

typescript
doc.insert(0, 'Hello');     // Insert at start
doc.insert(5, ' World');    // Insert at position 5
delete(position, length)

Delete text at the specified position.

typescript
doc.delete(0, 5);    // Delete first 5 characters
doc.delete(10, 3);   // Delete 3 characters starting at position 10
replace(position, length, text)

Replace text atomically (delete + insert).

typescript
doc.replace(0, 5, 'Hi');  // Replace first 5 chars with "Hi"
commit(description?)

Commit pending edits and create a journal entry.

typescript
doc.insert(0, 'Hello');
doc.commit('Add greeting');  // Creates a message and calls onSend

Note: If coalescing is enabled, edits are automatically committed after the coalescing delay. Explicit commit() calls bypass coalescing and commit immediately.

State Access

getState()

Get the current client state.

typescript
const state = doc.getState();
console.log(state.rope);           // TextRope instance
console.log(state.lamportTime);    // Current Lamport timestamp
console.log(state.vectorClock);    // Vector clock
console.log(state.journal);        // Journal entries
console.log(state.edits);          // Uncommitted edits
getText()

Get the current text content.

typescript
const text = doc.getText();  // "Hello World"
subscribe(listener)

Subscribe to state changes.

typescript
const unsubscribe = doc.subscribe(() => {
  console.log('Text changed:', doc.getText());
});

// Later: unsubscribe()

Server Communication

receive(msg)

Apply a message from another client or the server.

typescript
websocket.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  doc.receive(msg);
};
ack(msgId)

Mark a message as acknowledged by the server.

typescript
websocket.onmessage = (event) => {
  const { type, msgId } = JSON.parse(event.data);
  if (type === 'ack') {
    doc.ack(msgId);
  }
};
loadServerState(snapshot, journal)

Load state from the server (for initial sync or reconnection).

typescript
const { snapshot, journal } = await fetch('/api/document/123').then(r => r.json());
doc.loadServerState(snapshot, journal);

Undo/Redo

undo()

Undo the last committed change from this session.

typescript
const result = doc.undo();
if (result.msg) {
  // Send undo message to server
  websocket.send(JSON.stringify(result.msg));
}
redo()

Redo the last undone change from this session.

typescript
const result = doc.redo();
if (result.msg) {
  websocket.send(JSON.stringify(result.msg));
}

Coalescing Control

setCoalescingEnabled(enabled)

Enable or disable automatic operation coalescing.

typescript
doc.setCoalescingEnabled(true);
setCoalescingDelay(delayMs)

Set the coalescing delay in milliseconds.

typescript
doc.setCoalescingDelay(500);  // 500ms delay
getCoalescingEnabled()

Get current coalescing enabled state.

typescript
const enabled = doc.getCoalescingEnabled();
getCoalescingDelay()

Get current coalescing delay.

typescript
const delay = doc.getCoalescingDelay();

Type Definitions

TextMessage

A message containing text operations.

typescript
interface TextMessage {
  msgId: string;
  sessionId: string;
  operations: TextOperation[];
  vectorClock: VectorClock;
  lamportTime: number;
  timestamp: number;
  description?: string;
}

TextOperation

A text edit operation.

typescript
type TextOperation =
  | { type: 'insert'; op: InsertOp }
  | { type: 'delete'; op: DeleteOp };

TextDocumentState

The complete client-side state.

typescript
interface TextDocumentState {
  rope: TextRope;                    // Current text rope
  journal: TextJournalEntry[];        // Committed messages
  edits: TextEditBuffer;              // Uncommitted operations
  snapshot: TextSnapshot;             // Checkpoint
  lamportTime: number;
  vectorClock: VectorClock;
  sessionId: string;
}

Usage Patterns

Basic Single-User Editor

typescript
const doc = createTextDocument({ sessionId: 'user-1' });

// Simple text editing
doc.insert(0, 'Hello World');
doc.commit();

// Subscribe to changes
doc.subscribe(() => {
  updateUI(doc.getText());
});

Collaborative Editor with Server

typescript
const ws = new WebSocket('ws://server.com/doc/123');
const doc = createTextDocument({
  sessionId: generateSessionId(),
  coalescingEnabled: true,
  coalescingDelayMs: 300,
  onSend: (msg) => ws.send(JSON.stringify(msg)),
});

// Receive remote changes
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === 'text-update') {
    doc.receive(msg);
  } else if (msg.type === 'ack') {
    doc.ack(msg.msgId);
  }
};

// Make local changes
textarea.oninput = (e) => {
  const newText = e.target.value;
  const oldText = doc.getText();

  // Compute diff and apply
  const diff = computeDiff(oldText, newText);
  if (diff.delete) doc.delete(diff.position, diff.delete);
  if (diff.insert) doc.insert(diff.position, diff.insert);

  // Auto-committed after coalescing delay
};

React Integration

typescript
function useTextDocument(docId: string) {
  const [text, setText] = useState('');
  const docRef = useRef<TextDocumentStore>();

  useEffect(() => {
    const ws = new WebSocket(`ws://server/doc/${docId}`);
    const doc = createTextDocument({
      sessionId: generateSessionId(),
      coalescingEnabled: true,
      onSend: (msg) => ws.send(JSON.stringify(msg)),
    });

    const unsub = doc.subscribe(() => {
      setText(doc.getText());
    });

    ws.onmessage = (e) => {
      const msg = JSON.parse(e.data);
      if (msg.type === 'text-update') doc.receive(msg);
      if (msg.type === 'ack') doc.ack(msg.msgId);
    };

    docRef.current = doc;

    return () => {
      unsub();
      ws.close();
    };
  }, [docId]);

  const insert = (pos: number, text: string) => {
    docRef.current?.insert(pos, text);
  };

  const deleteText = (pos: number, len: number) => {
    docRef.current?.delete(pos, len);
  };

  return { text, insert, deleteText };
}

Comparison with Graph API

The Text Document API follows the same patterns as the Graph API:

FeatureGraph APIText Document API
FactorycreateGraph()createTextDocument()
StateClientState with SceneGraphTextDocumentState with TextRope
Editedit(op)insert(), delete(), replace()
Commitcommit(description)commit(description)
Syncreceive(msg), ack(msgId)receive(msg), ack(msgId)
Undo/Redoundo(), redo()undo(), redo()
CoalescingsetCoalescingEnabled()setCoalescingEnabled()
Subscribesubscribe(listener)subscribe(listener)

Best Practices

Performance

  1. Enable Coalescing for real-time typing scenarios to batch rapid edits
  2. Use replace() instead of separate delete + insert for selection replacement
  3. Subscribe Sparingly - Only subscribe where needed to avoid unnecessary renders

Conflict Resolution

  1. Trust the CRDT - The rope automatically handles concurrent edits correctly
  2. Use Vector Clocks - They track causality and prevent duplicate application
  3. Preserve Journal - Keep journal entries for proper undo/redo semantics

Server Integration

  1. Send Messages Immediately - Use the onSend callback to forward operations
  2. Acknowledge Messages - Call ack() when the server confirms receipt
  3. Load Initial State - Use loadServerState() for reconnection and initial sync
  4. Cleanup on Disconnect - Close WebSocket connections properly

See Also