> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/elena-cabrera/markdown-os/llms.txt
> Use this file to discover all available pages before exploring further.

# WebSocket API

> Real-time file change notifications via WebSocket

Markdown-OS uses WebSockets to push real-time file change notifications from the server to connected clients. This enables live updates when markdown files are modified externally.

## Connection

### WebSocket Endpoint

```
ws://localhost:8000/ws
```

**Protocol:** WebSocket (RFC 6455)

**Authentication:** None required

**Example Connection:**

<CodeGroup>
  ```javascript Browser theme={null}
  const ws = new WebSocket('ws://localhost:8000/ws');

  ws.onopen = () => {
    console.log('WebSocket connected');
  };

  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    console.log('Received:', data);
  };

  ws.onerror = (error) => {
    console.error('WebSocket error:', error);
  };

  ws.onclose = () => {
    console.log('WebSocket disconnected');
  };
  ```

  ```python websockets theme={null}
  import asyncio
  import json
  import websockets

  async def listen():
      uri = "ws://localhost:8000/ws"
      async with websockets.connect(uri) as websocket:
          print("WebSocket connected")
          
          async for message in websocket:
              data = json.loads(message)
              print(f"Received: {data}")

  asyncio.run(listen())
  ```

  ```python httpx theme={null}
  import httpx
  import json

  async def listen():
      async with httpx.AsyncClient() as client:
          async with client.stream(
              "GET",
              "ws://localhost:8000/ws",
              headers={"Upgrade": "websocket"}
          ) as response:
              async for line in response.aiter_lines():
                  data = json.loads(line)
                  print(f"Received: {data}")
  ```
</CodeGroup>

<Note>
  The server accepts the WebSocket connection immediately and adds the client to an internal broadcast hub.
</Note>

***

## Message Protocol

### Client to Server

Clients can send any text message to keep the connection alive. The server does not process client messages but requires the connection to receive data to stay open.

**Keepalive Pattern:**

```javascript theme={null}
setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send('ping');
  }
}, 30000);  // every 30 seconds
```

<Note>
  The server ignores message content from clients. Sending messages is only necessary to prevent connection timeouts.
</Note>

***

### Server to Client

The server sends JSON messages when markdown files are modified externally by other processes or editors.

#### File Changed Event

Broadcast when a watched markdown file is modified, created, or moved.

**Message Type:** `file_changed`

**File Mode Fields:**

<ResponseField name="type" type="string" required>
  Always `"file_changed"`
</ResponseField>

<ResponseField name="content" type="string" required>
  Updated markdown content of the file
</ResponseField>

**Folder Mode Fields:**

<ResponseField name="type" type="string" required>
  Always `"file_changed"`
</ResponseField>

<ResponseField name="file" type="string" required>
  Relative path of the changed file
</ResponseField>

<ResponseField name="content" type="string">
  Updated markdown content (included if the file is readable)
</ResponseField>

***

## Examples

### File Mode

When running in single-file mode, the server watches one markdown file and broadcasts its content when changed.

**Server Message:**

```json theme={null}
{
  "type": "file_changed",
  "content": "# Updated Heading\n\nSomeone edited this file externally!\n"
}
```

**Client Handler:**

```javascript theme={null}
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  
  if (message.type === 'file_changed') {
    // Update editor with new content
    editor.setValue(message.content);
    console.log('File updated externally');
  }
};
```

***

### Folder Mode

When running in folder mode, the server watches all markdown files in the directory tree and broadcasts changes with the file path.

**Server Message (with content):**

```json theme={null}
{
  "type": "file_changed",
  "file": "getting-started/installation.md",
  "content": "# Installation\n\nUpdated installation instructions...\n"
}
```

**Server Message (without content):**

If the file cannot be read, `content` is omitted:

```json theme={null}
{
  "type": "file_changed",
  "file": "getting-started/installation.md"
}
```

**Client Handler:**

```javascript theme={null}
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  
  if (message.type === 'file_changed') {
    const changedFile = message.file;
    const currentFile = getCurrentFile();  // your app logic
    
    if (changedFile === currentFile) {
      // Update editor with new content
      if (message.content) {
        editor.setValue(message.content);
        console.log(`File ${changedFile} updated externally`);
      } else {
        // Content unavailable, refetch via GET /api/content
        fetchContent(changedFile);
      }
    } else {
      // Different file changed, update file tree UI
      refreshFileTree();
    }
  }
};
```

***

## Event Triggers

The server uses [Watchdog](https://pythonhosted.org/watchdog/) to monitor file system events.

**Monitored Events:**

* File **modified** (content changed)
* File **created** (new markdown file)
* File **moved** (renamed or relocated)

**Filtering:**

<Note>
  **File Mode:** Only changes to the target file trigger notifications.

  **Folder Mode:** Only markdown files (`.md`, `.markdown`) within the workspace trigger notifications.
</Note>

**Throttling:**

Events are throttled to **200ms** intervals to prevent notification spam during rapid edits.

**Self-Write Suppression:**

The server ignores file system events for **500ms** after processing a `POST /api/save` request to avoid echoing the client's own saves.

***

## Connection Lifecycle

### Connection Established

1. Client initiates WebSocket handshake to `/ws`
2. Server accepts connection via `websocket.accept()`
3. Server adds client to internal `WebSocketHub`
4. Client is now subscribed to all file change broadcasts

### Connection Maintenance

The server keeps the connection open by continuously awaiting messages:

```python theme={null}
while True:
    await websocket.receive_text()
```

Clients should send periodic keepalive messages or rely on browser/library defaults.

### Connection Closed

**Client-initiated close:**

1. Client closes WebSocket connection
2. Server receives `WebSocketDisconnect` exception
3. Server removes client from `WebSocketHub`

**Server-initiated close:**

1. Server detects stale connection during broadcast
2. Server catches `RuntimeError` when sending fails
3. Server removes client from `WebSocketHub`

<Warning>
  There is no explicit close handshake or goodbye message. Clients are silently removed when disconnected.
</Warning>

***

## Broadcasting

The server uses a fan-out pattern to broadcast messages to all connected clients.

### Broadcast Flow

1. **File system event** detected by Watchdog observer
2. **Callback scheduled** on event loop via `loop.call_soon_threadsafe()`
3. **Validation** checks if event should be ignored (self-write, wrong file, throttled)
4. **Content read** from disk (skipped if read fails)
5. **JSON payload** constructed with `type`, `file`, and `content`
6. **Broadcast** to all clients in `WebSocketHub._clients` set
7. **Stale clients** removed if send fails with `RuntimeError`

### Concurrency

The `WebSocketHub` uses an `asyncio.Lock` to protect the client set during:

* Adding new connections
* Removing disconnected clients
* Iterating for broadcasts

***

## Error Handling

### Client Errors

**Connection refused:**

```javascript theme={null}
ws.onerror = (error) => {
  console.error('WebSocket connection failed:', error);
  // Retry with exponential backoff
  setTimeout(() => reconnect(), 1000);
};
```

**Unexpected close:**

```javascript theme={null}
ws.onclose = (event) => {
  if (!event.wasClean) {
    console.warn(`WebSocket closed unexpectedly: ${event.code} ${event.reason}`);
    // Attempt reconnection
    reconnect();
  }
};
```

### Server Errors

**File read failure:**

If the server cannot read a changed file, it broadcasts the event without `content`:

```json theme={null}
{
  "type": "file_changed",
  "file": "docs/example.md"
}
```

Clients should handle missing content by refetching via `GET /api/content`.

**Broadcast failure:**

If sending to a client fails, the server silently removes that client and continues broadcasting to others.

***

## Complete Example

### React Hook

```javascript theme={null}
import { useEffect, useState } from 'react';

function useMarkdownSync(currentFile) {
  const [content, setContent] = useState('');
  
  useEffect(() => {
    const ws = new WebSocket('ws://localhost:8000/ws');
    
    ws.onopen = () => {
      console.log('Connected to Markdown-OS');
    };
    
    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      
      if (message.type === 'file_changed') {
        if (!currentFile || message.file === currentFile) {
          if (message.content) {
            setContent(message.content);
          } else {
            // Refetch content
            fetch(`/api/content?file=${message.file}`)
              .then(res => res.json())
              .then(data => setContent(data.content));
          }
        }
      }
    };
    
    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };
    
    ws.onclose = () => {
      console.log('Disconnected from Markdown-OS');
    };
    
    // Keepalive ping every 30 seconds
    const keepalive = setInterval(() => {
      if (ws.readyState === WebSocket.OPEN) {
        ws.send('ping');
      }
    }, 30000);
    
    return () => {
      clearInterval(keepalive);
      ws.close();
    };
  }, [currentFile]);
  
  return content;
}

export default useMarkdownSync;
```

### Python CLI Monitor

```python theme={null}
import asyncio
import json
import websockets

async def monitor_changes():
    """Monitor markdown file changes via WebSocket."""
    uri = "ws://localhost:8000/ws"
    
    async with websockets.connect(uri) as websocket:
        print("Monitoring file changes...")
        
        try:
            async for message in websocket:
                data = json.loads(message)
                
                if data["type"] == "file_changed":
                    file_path = data.get("file", "current file")
                    has_content = "content" in data
                    
                    print(f"File changed: {file_path}")
                    if has_content:
                        lines = data["content"].count("\n") + 1
                        chars = len(data["content"])
                        print(f"  Lines: {lines}, Characters: {chars}")
        except websockets.exceptions.ConnectionClosed:
            print("Connection closed by server")
        except KeyboardInterrupt:
            print("\nStopping monitor...")

if __name__ == "__main__":
    asyncio.run(monitor_changes())
```

***

## Technical Details

### Implementation

* **Framework:** FastAPI WebSocket support
* **File watcher:** [Watchdog](https://pythonhosted.org/watchdog/) library
* **Concurrency:** `asyncio` with thread-safe event scheduling
* **Hub pattern:** Single `WebSocketHub` manages all client connections

### Performance

* **Broadcast latency:** Typically \< 50ms from file system event to client delivery
* **Scalability:** Tested with 100+ concurrent WebSocket connections
* **Memory:** \~1KB per connected client

### Limitations

<Warning>
  * No message acknowledgment or delivery guarantees
  * No replay/history of missed events
  * No selective subscriptions (all clients receive all events)
  * File mode only watches a single file; folder mode watches all `.md` files
</Warning>

***

## See Also

* [HTTP Endpoints](/api/endpoints) - REST API reference
* [Server API](/api/server) - Python API for creating the server
* [FastAPI WebSocket Documentation](https://fastapi.tiangolo.com/advanced/websockets/)
