# 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

// 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();

  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:

| Feature | Graph API | Text Document API |
|---------|-----------|-------------------|
| **Factory** | `createGraph()` | `createTextDocument()` |
| **State** | `ClientState` with `SceneGraph` | `TextDocumentState` with `TextRope` |
| **Edit** | `edit(op)` | `insert()`, `delete()`, `replace()` |
| **Commit** | `commit(description)` | `commit(description)` |
| **Sync** | `receive(msg)`, `ack(msgId)` | `receive(msg)`, `ack(msgId)` |
| **Undo/Redo** | `undo()`, `redo()` | `undo()`, `redo()` |
| **Coalescing** | `setCoalescingEnabled()` | `setCoalescingEnabled()` |
| **Subscribe** | `subscribe(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

- [TextRope CRDT](/rope.md) - Low-level rope operations
- [React Hooks](/react-hooks.md) - React integration patterns
- [Server API](/server.md) - Server-side integration
- [Live Demo](/rope-demo) - Interactive demo
