Aller au contenu principal

Chapter 8: Collaborative Editing

Overview

Collaborative editing is one of the most popular applications of CRDTs. It enables multiple users to edit the same document simultaneously while maintaining consistency and resolving conflicts automatically.

Text CRDTs

Operational Transformation vs. CRDTs

Traditional collaborative editing often uses Operational Transformation (OT), but CRDTs provide several advantages:

  • No Central Server: CRDTs work in peer-to-peer scenarios
  • Automatic Conflict Resolution: Conflicts are resolved deterministically
  • Offline Support: Users can continue editing without network connectivity

Yjs is a high-performance CRDT implementation for collaborative editing.

Basic Usage

import * as Y from 'yjs'

// Create a new document
const doc = new Y.Doc()

// Create a text type
const text = doc.getText('content')

// Insert text
text.insert(0, 'Hello, World!')

// Observe changes
text.observe(event => {
console.log('Text changed:', event.changes)
})

// Get the current content
console.log(text.toString())

Collaborative Editing

// User A
const docA = new Y.Doc()
const textA = docA.getText('content')
textA.insert(0, 'Hello')

// User B
const docB = new Y.Doc()
const textB = docB.getText('content')
textB.insert(0, 'Hi')

// Synchronize (simplified)
// In practice, you'd use a provider like WebSocket or WebRTC
docA.merge(docB)
docB.merge(docA)

// Both documents now contain "HiHello" or "HelloHi"

Rich Text Editing

Quill.js Integration

import Quill from 'quill'
import QuillCursors from 'quill-cursors'
import * as Y from 'yjs'
import { QuillBinding } from 'y-quill'

Quill.register('modules/cursors', QuillCursors)

const doc = new Y.Doc()
const text = doc.getText('quill')

const quill = new Quill('#editor', {
modules: {
cursors: true,
toolbar: [
['bold', 'italic', 'underline'],
['link', 'blockquote', 'code-block'],
[{ list: 'ordered' }, { list: 'bullet' }]
]
}
})

const binding = new QuillBinding(text, quill, doc)

Code Editing

Monaco Editor Integration

import * as monaco from 'monaco-editor'
import * as Y from 'yjs'
import { MonacoBinding } from 'y-monaco'

const doc = new Y.Doc()
const text = doc.getText('monaco')

const editor = monaco.editor.create(document.getElementById('editor'), {
value: '',
language: 'javascript',
theme: 'vs-dark'
})

const binding = new MonacoBinding(text, editor.getModel(), new Set([editor]), doc)

Conflict Resolution Strategies

Last-Write-Wins (LWW)

Simple but can lose user input:

class LWWRegister {
constructor() {
this.value = null
this.timestamp = 0
}

set(value, timestamp) {
if (timestamp > this.timestamp) {
this.value = value
this.timestamp = timestamp
}
}

merge(other) {
if (other.timestamp > this.timestamp) {
this.value = other.value
this.timestamp = other.timestamp
}
}
}

Add-Wins Set

Preserves all additions:

class AddWinsSet {
constructor() {
this.elements = new Map() // element -> timestamp
}

add(element, timestamp) {
const existing = this.elements.get(element)
if (!existing || timestamp > existing) {
this.elements.set(element, timestamp)
}
}

remove(element) {
this.elements.delete(element)
}

merge(other) {
for (const [element, timestamp] of other.elements) {
const existing = this.elements.get(element)
if (!existing || timestamp > existing) {
this.elements.set(element, timestamp)
}
}
}
}

Performance Considerations

Delta Compression

Instead of sending full documents, send only changes:

class DeltaCompressor {
static compress(changes) {
// Compress changes to minimize network traffic
return changes.map(change => ({
type: change.type,
position: change.position,
content: change.content,
timestamp: change.timestamp
}))
}

static decompress(compressed) {
// Decompress changes back to full format
return compressed.map(change => ({
type: change.type,
position: change.position,
content: change.content,
timestamp: change.timestamp
}))
}
}

Batching Updates

Group multiple changes to reduce synchronization overhead:

class UpdateBatcher {
constructor(delay = 100) {
this.pendingUpdates = []
this.timer = null
this.delay = delay
}

addUpdate(update) {
this.pendingUpdates.push(update)

if (this.timer) {
clearTimeout(this.timer)
}

this.timer = setTimeout(() => {
this.flush()
}, this.delay)
}

flush() {
if (this.pendingUpdates.length > 0) {
// Send batched updates
this.sendUpdates(this.pendingUpdates)
this.pendingUpdates = []
}
}
}

Testing Collaborative Editing

Conflict Simulation

function simulateConflict() {
const docA = new Y.Doc()
const docB = new Y.Doc()

const textA = docA.getText('content')
const textB = docB.getText('content')

// Simulate concurrent edits
textA.insert(0, 'Hello')
textB.insert(0, 'Hi')

// Simulate network delay
setTimeout(() => {
docA.merge(docB)
docB.merge(docA)

console.log('Document A:', textA.toString())
console.log('Document B:', textB.toString())
}, 100)
}

Next Steps

In the next chapter, we'll explore how CRDTs integrate with distributed databases.