Skip to content

Collaborative Editing

The :collab module implements the client side of collaborative editing using operational transformation.

Setup

Install the collab() extension with your starting document version. Here the demo creates two editors that will synchronize:

    val sessionA = rememberEditorSession(
        doc = SampleDocs.collabInitial,
        extensions = showcaseSetup + javascript().extension +
            collab(CollabConfig(clientID = "editor-a"))
    )
    val sessionB = rememberEditorSession(
        doc = SampleDocs.collabInitial,
        extensions = showcaseSetup + javascript().extension +
            collab(CollabConfig(clientID = "editor-b"))
    )

CollabConfig

data class CollabConfig(
    val startVersion: Int = 0,
    val clientID: String? = null,
    val sharedEffects: ((Transaction) -> List<StateEffect<*>>)? = null
)
Property Description
startVersion The document version when the editor was initialized
clientID Unique client identifier (auto-generated if null)
sharedEffects Extract effects from transactions to share with other clients

Synchronization logic

The demo uses a shared update list as a simple "server". Each client sends its local changes and receives remote ones:

    fun syncOne(session: com.monkopedia.kodemirror.view.EditorSession) {
        val version = getSyncedVersion(session.state)
        val pending = sharedUpdates.drop(version)
        if (pending.isNotEmpty()) {
            session.dispatch(receiveUpdates(session.state, pending))
        }
    }

    fun sendOne(session: com.monkopedia.kodemirror.view.EditorSession) {
        val sendable = sendableUpdates(session.state)
        for (u in sendable) {
            sharedUpdates.add(
                Update(
                    changes = u.changes,
                    clientID = u.clientID,
                    effects = u.effects
                )
            )
        }
    }

SendableUpdate

Each SendableUpdate contains:

data class SendableUpdate(
    val changes: ChangeSet,
    val clientID: String,
    val effects: List<StateEffect<*>> = emptyList(),
    val origin: Transaction
)

Update

Each Update from the server:

data class Update(
    val changes: ChangeSet,
    val clientID: String,
    val effects: List<StateEffect<*>> = emptyList()
)

Querying state

// Current synced version
val version = getSyncedVersion(state)

// This client's ID
val id = getClientID(state)

Rebasing

If the server rejects an update because it was based on a stale version, use rebaseUpdates to transform it over the accepted updates:

val rebased = rebaseUpdates(rejectedUpdates, acceptedUpdates)

Full example: polling architecture

A simple polling-based collaboration setup:

class CollabClient(
    private val view: EditorSession,
    private val serverUrl: String
) {
    suspend fun push() {
        val updates = sendableUpdates(view.state)
        if (updates.isNotEmpty()) {
            // Serialize and send to server
            pushToServer(serverUrl, updates, getSyncedVersion(view.state))
        }
    }

    suspend fun pull() {
        val version = getSyncedVersion(view.state)
        val updates = pullFromServer(serverUrl, version)
        if (updates.isNotEmpty()) {
            val spec = receiveUpdates(view.state, updates)
            view.dispatch(spec)
        }
    }
}

API summary

Function Description
collab(config) Install collaborative editing extension
sendableUpdates(state) Get pending local updates to send
receiveUpdates(state, updates) Build a TransactionSpec from remote updates
getSyncedVersion(state) Get the current synced version number
getClientID(state) Get this client's ID
rebaseUpdates(updates, over) Rebase out-of-date updates

Based on the CodeMirror Collaborative Editing example.