This project was implemented as a part of the Distributed Systems (CS3.401) coursework at IIIT Hyderabad by
- M P Samartha (2023102038)
- Vinay R (2019112020)
A fully distributed collaborative text editor implementing Conflict-Free Replicated Data Types (CRDTs). This project demonstrates how to build a system where multiple users can edit the same document concurrently without a central server resolving conflicts.
- Real-time collaborative editing - Multiple users can edit the same document simultaneously
- Two CRDT implementations - LWW (state-based) and WOOT (operation-based)
- Rich-text formatting - Bold, italic, underline, text color, highlight
- Live cursor tracking + user list - See who is editing and where
- Local undo/redo - Per-client undo/redo stacks
- State persistence - Documents are automatically saved to disk
- Causal ordering - Vector clocks ensure causally consistent updates
- Benchmark harness - Latency, bandwidth, throughput measurements + notebook visualization
The system consists of three layers:
- LWW Element Set (state-based CRDT)
- Each character assigned a unique
(site_id, logical_timestamp)pair - Order is maintained with a floating
ordervalue to insert between neighbors - Deterministic merge ensures convergence
- Vector clocks prevent causal inconsistencies
- Each character assigned a unique
- WOOT Sequence CRDT (operation-based CRDT)
- Inserts reference
prev_idandnext_idneighbors - Missing dependencies are queued and integrated later
- Tombstones preserve deletions while keeping IDs stable
- Inserts reference
- WebSocket relay server - Routes operations between clients
- Dumb routing - Server doesn't resolve conflicts; just relays messages
- JSON-based messaging - Simple and debuggable protocol
- Connection management - Handles client add/remove with graceful cleanup
- Vue.js UI - Responsive, modern editor interface
- Client-side CRDT - Each client maintains local CRDT state
- Immediate feedback - Local edits appear instantly
- Automatic sync - Changes synced to server and other clients
- Formatting + cursors - Rich text styling and remote cursor labels
- Backend: Python 3.11+, WebSockets (websockets library)
- Frontend: HTML5, Vue.js 3, vanilla JavaScript
- Storage: JSON files (in
./data/directory) - Testing: pytest
- Benchmarking: Python multiprocessing, CSV outputs, Jupyter notebook visualization
.
├── backend/
│ ├── crdt/
│ │ ├── base.py # Abstract CRDT interface
│ │ ├── factory.py # CRDT factory + type normalization
│ │ ├── lww_set.py # LWW CRDT implementation
│ │ ├── vector_clock.py # Causal ordering
│ │ └── woot.py # WOOT CRDT implementation
│ ├── networking/
│ │ ├── server.py # WebSocket server
│ │ ├── message.py # Message serialization
│ │ └── document_manager.py # Document state management
│ ├── storage/
│ │ └── persistence.py # JSON persistence layer
│ ├── main.py # Server entry point
│ ├── config.py # Configuration constants
│ └── requirements.txt # Python dependencies
├── benchmarks/
│ ├── bench_client.py # Headless benchmark client
│ ├── metrics.py # Aggregation helpers
│ ├── run_benchmarks.py # Benchmark runner
│ └── workloads.py # Workload generators
├── data/
├── frontend/
│ ├── index.html # Editor UI
│ ├── styles.css # Styling
│ ├── editor.js # Vue.js app and UI logic
│ └── crdt-client.js # Client-side CRDT implementation
├── tests/
│ ├── test_integration.py # Integration tests
│ ├── test_lww_crdt.py # LWW CRDT tests
│ ├── test_vector_clock.py # Vector Clock tests
│ └── test_woot_crdt.py # WOOT CRDT tests
├── benchmark_results.ipynb
├── run_server.py
├── report.tex
├── README.md
└── .gitignore
- Python 3.11 or higher
- pip (Python package manager)
- Modern web browser (Chrome, Firefox, Safari, Edge)
# Install Python dependencies
cd backend
pip install -r requirements.txt
# Create data directory for persistence
mkdir -p ../dataNo additional setup needed! The frontend is pure HTML/JS/CSS with Vue.js loaded from CDN.
Open TWO terminal windows:
Terminal 1: Start the Backend Server
python run_server.py --crdt lwwUse --crdt woot to switch the server to WOOT:
python run_server.py --crdt wootYou should see:
INFO:backend.main:Starting Collaborative Text Editor Server
INFO:backend.main:Listening on ws://localhost:8765
Terminal 2: Start the Frontend HTTP Server
cd frontend
python -m http.server 8000You should see:
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
- Open your browser and navigate to: http://localhost:8000
- You should see the login modal
- Enter a Document ID (e.g., "my-doc")
- Enter your name (or leave blank for auto-generated name)
- Click Connect
- Open http://localhost:8000 in multiple browser tabs/windows
- Connect each to the same document ID
- Type in one editor - see it appear in others instantly!
Option 2: Using Python module
python -m backend.main --crdt lwwOption 3: From backend directory
cd backend
python -m main --crdt lww# From project root
python -m pytest tests/ -v# Vector Clock tests only
python -m pytest tests/test_vector_clock.py -v
# LWW CRDT tests only
python -m pytest tests/test_lww_crdt.py -v
# WOOT CRDT tests only
python -m pytest tests/test_woot_crdt.py -v
# Integration tests
python -m pytest tests/test_integration.py -v
# Run with coverage
python -m pytest tests/ --cov=backend --cov-report=htmlThe benchmark harness runs the server locally and spawns multiple headless clients to measure latency, bandwidth, throughput, and convergence time for both LWW and WOOT under different workloads and scale settings.
python -m benchmarks.run_benchmarksDefault matrix:
- CRDTs: lww, woot
- Clients: 3, 4
- Ops per client: 10, 100, 500
- Doc sizes: 1000, 5000, 10000
- Workloads: sequential, concurrent_same, concurrent_random, mixed
python -m benchmarks.run_benchmarks \
--crdt lww,woot \
--clients 3,4 \
--ops 10,100,500 \
--doc-size 1000,5000,10000 \
--workloads sequential,concurrent_same,concurrent_random,mixed \
--output-dir benchmarks/results \
-j 2Each run produces two CSV files under benchmarks/results/:
<run_id>_summary.csv— p50/p95/p99 latency, bandwidth totals, throughput, and convergence time.<run_id>_latencies.csv— per-op latency samples for plotting.
The notebook Team26_GossipGang_Benchmark_Results.ipynb contains tables and plots for comparing LWW vs WOOT across the default matrix.
- Benchmarks run the WebSocket server automatically per CRDT. Use
--reuse-serverif you want to start the server manually. - Convergence time is measured as the delay between the last sent operation and the last applied operation across all clients.
-
User 1: Opens browser, connects to document "collab-doc"
- Types: "Hello"
- Content: "Hello"
-
User 2: Opens another browser, connects to same "collab-doc"
- Receives initial state: "Hello"
- Types: " World"
- Content: "Hello World"
-
User 1: Receives User 2's changes instantly
- Content: "Hello World"
-
User 1: Deletes "World", types "CRDT"
- Content: "Hello CRDT"
-
User 2: Sees the change
- Content: "Hello CRDT"
No conflicts! CRDTs automatically handle concurrent edits.
// Connect to document
{
"type": "connect",
"doc_id": "my-doc",
"user_id": 12345,
"site_id": 67890,
"data": { "username": "Alice" }
}
// Insert character (LWW)
{
"type": "insert",
"doc_id": "my-doc",
"user_id": 12345,
"site_id": 67890,
"data": {
"position": 0,
"char": "a",
"timestamp": 1,
"id": "67890:1",
"order": 0.5,
"attrs": { "bold": false, "italic": false, "underline": false }
}
}
// Insert character (WOOT)
{
"type": "insert",
"doc_id": "my-doc",
"user_id": 12345,
"site_id": 67890,
"data": {
"id": "67890:5",
"char": "a",
"prev_id": "HEAD",
"next_id": "TAIL",
"attrs": { "bold": false, "italic": false, "underline": false }
}
}
// Delete character (LWW)
{
"type": "delete",
"doc_id": "my-doc",
"user_id": 12345,
"site_id": 67890,
"data": { "position": 0, "timestamp": 2, "id": "67890:1" }
}
// Delete character (WOOT)
{
"type": "delete",
"doc_id": "my-doc",
"user_id": 12345,
"site_id": 67890,
"data": { "id": "67890:5" }
}
// Format range
{
"type": "format",
"doc_id": "my-doc",
"user_id": 12345,
"site_id": 67890,
"data": {
"ids": ["67890:1", "67890:2"],
"attrs": { "bold": true }
}
}
// Move cursor
{
"type": "cursor_move",
"doc_id": "my-doc",
"user_id": 12345,
"site_id": 67890,
"data": { "position": 5, "username": "Alice", "client_id": 1 }
}// Ack (assigned client ID)
{
"type": "ack",
"doc_id": "my-doc",
"user_id": 12345,
"site_id": 67890,
"data": { "client_id": 1 }
}
// State sync (on connection)
{
"type": "state_sync",
"doc_id": "my-doc",
"user_id": 0,
"site_id": 0,
"data": {
"crdt_type": "lww",
"crdt_state": {
"site_id": 67890,
"logical_clock": 10,
"elements": { ... }
}
}
}
// User list update
{
"type": "user_list",
"doc_id": "my-doc",
"user_id": 0,
"site_id": 0,
"data": {
"users": [
{ "client_id": 1, "username": "Alice", "cursor_position": 5, "user_id": 12345 }
]
}
}Each character is tagged with:
- value: The character
- site_id and timestamp: A unique ID per insertion
- order: A floating-point position value used to insert between neighbors
- deleted: Tombstone flag
On merge, replicas combine their element sets deterministically, using vector clocks to preserve causal ordering and last-write-wins semantics for conflicts.
Each character is a node with a stable ID and two references:
- prev_id / next_id: Neighbor pointers used to place the character
- visible: Whether the character is deleted (tombstone)
If an insert arrives before its neighbors, it is queued in a pending list until dependencies are present. This yields deterministic convergence without a central transformer.
- Single relay server (no P2P gossip protocol yet)
- No offline queue (edits require an active connection)
- No authentication or access control
- Single-document UI (one document per client session)
- WOOT integration is not optimized (linear scans under heavy concurrency)
- P2P Gossip Sync - Peer-to-peer document sync
- Offline operation queue - Replay local edits on reconnect
- Authentication - Access control per document
- WOOT indexing - Faster insert integration using indexed structures
- Multi-document support - Edit multiple docs per client
- Benchmark automation - Extended workloads and visualization reports
Edit backend/config.py to customize:
# Server
SERVER_HOST = "localhost"
SERVER_PORT = 8765
# Document
MAX_DOCUMENT_SIZE = 10 * 1024 * 1024 # 10MB
# Storage
STORAGE_DIR = "./data"
AUTO_PERSIST_INTERVAL = 30 # seconds
# Logging
LOG_LEVEL = "INFO"This happens when you open the HTML file directly with file:// protocol instead of via HTTP.
Solution:
-
✗ Do NOT do this:
- Double-click
frontend/index.html - Use
file:///path/to/index.html
- Double-click
-
✓ Do this instead:
cd frontend python -m http.server 8000 # Then open http://localhost:8000 in browser
- Ensure backend server is running on ws://localhost:8765
- Check that you ran
python run_server.py --crdt lww(or--crdt woot) in the first terminal - Check firewall settings
- Try a different port in config.py
- Check browser console (F12 → Console tab) for detailed error messages
- Open browser developer tools: Press F12
- Go to Console tab: You'll see logs like:
Connecting to WebSocket: ws://localhost:8765WebSocket connected(if successful)- Error messages if connection fails
- Check Network tab: Look for the WebSocket connection (filter by "ws")
- Should see
ws://localhost:8765with status 101 Switching Protocols
- Should see
- Server logs: Check the terminal running
python run_server.py --crdt lwwfor server-side errors
- ✓ Check that both servers are running:
- Backend:
python run_server.py --crdt lww - Frontend:
cd frontend && python -m http.server 8000
- Backend:
- Check browser console (F12) for detailed error
- Verify URLs:
- Frontend:
http://localhost:8000(not file://) - WebSocket:
ws://localhost:8765
- Frontend:
- Try a fresh page reload (Ctrl+Shift+R or Cmd+Shift+R)
- Check that firewall allows connections to ports 8000 and 8765
- The server enforces a single CRDT type. Restart the server with the same
--crdtoption for all clients and documents.
- Check that
./data/directory exists and is writable - Look for errors in server console
- Verify STORAGE_DIR in config.py
- Cursor sync is for UI display; documents always converge
- Check browser console for WebSocket errors
- Ensure pytest and pytest-asyncio are installed:
pip install -r backend/requirements.txt - Run tests from project root:
cd /path/to/project - Use
-p no:asyncioflag to skip async plugin issues
python -m pytest tests/ -v -p no:asyncioValidates:
- Vector clock causal ordering
- LWW CRDT insertion/deletion
- WOOT CRDT insertion/deletion
- State merging and convergence
- Serialization/deserialization
Test 1: Concurrent Inserts
- Open two browsers to "test-doc"
- User 1 types "A", User 2 types "B" simultaneously
- Both see "AB" (deterministic ordering by site_id) ✓ Pass: Document converges without conflicts
Test 2: Formatting Propagation
- Select text in User 1 and toggle bold/highlight
- User 2 sees the same formatting ✓ Pass: Formatting converges across clients
See benchmark_results.ipynb and benchmarks/results/ for measured latency,
bandwidth, throughput, and convergence time across workloads.
This implementation is based on:
- CRDT Theory: Shapiro et al., "Conflict-free Replicated Data Types"
- LWW Element Set: Common state-based CRDT design
- Vector Clocks: Lamport, "Time, Clocks, and the Ordering of Events in a Distributed System"
MIT License - feel free to use and modify for your projects!