CollaborationManager is a plugin (@editorjs/collaboration-manager) registered in Core during setup. It bridges EditorJSModel and OTClient (WebSocket client):
- Subscribes to model change events and converts them into
Operationmessages sent over WebSocket. - Listens for incoming remote operations and applies them back to the model.
- Batches rapid operations (500ms debounce window) and forwards completed batches to its own
UndoRedoManager. - On
CoreEventType.Ready, connects to the OT server if server config is provided.
When concurrent edits start from revision N, reconciliation is two-step:
- Step 1: transform each pending local op against the incoming remote op.
- Step 2: transform the remote op against pending local ops before applying locally.
This preserves convergence regardless of arrival order.
→ diagrams/collaboration-ot-flow.mmd
Handshake, local send/ack path, remote OT transform path, then local apply.
OTClient and OTServer exchange JSON messages over WebSocket. Every message has the shape:
{ "type": "<MessageType>", "payload": { ... } }type |
Direction | Payload fields | Description |
|---|---|---|---|
Handshake |
Client → Server | document (DocumentId), userId, rev, data? (EditorDocumentSerialized) |
Connect to a document. Client sends its current state on first connect. |
Handshake |
Server → Client | document, userId, rev, data? |
Server echoes back. If the server already has the document, data contains the authoritative state and the client should call initializeDocument(data). |
Operation |
Client → Server | Serialized Operation (type, index, data, userId, rev) |
A local operation to apply. |
Operation |
Server → All clients | Serialized transformed Operation |
Server broadcasts the transformed operation to every client in the document room (including the author — the author's copy serves as an ack). |
OTServer maintains one DocumentManager per documentId. On receiving an Operation:
- If
operation.revis ahead of the server's current revision the operation is rejected (WebSocket closed with code4400). - Conflicting operations (all ops with
rev >= operation.rev) are fetched and the incoming op is transform-reduced through them. - The transformed op is applied to the server's
EditorJSModelcopy and the revision counter is incremented. - The transformed op (with the new
rev) is broadcast to all clients in the document room.
Operations are processed sequentially per document — DocumentManager queues them so a new op always awaits the previous one.
There are two undo/redo systems:
UndoRedoManager in Core listens to document model events directly, groups consecutive changes by a 500ms debounce window, and stores logical steps on stacks.
Undo/redo inverts and re-applies the stored events while suppressing re-record to avoid stack pollution.
This manager respects UndoCoreEvent and RedoCoreEvent — if either event's defaultPrevented is true, the manager skips the operation, allowing other handlers (like CollaborationManager) to take precedence.
UndoRedoManager in Collaboration stores Operation instances and inverts them for the OT pipeline.
BatchedOperation groups rapid single-character inserts or deletes on the same data key into one logical edit for better history granularity. Insert operations are batched when each character is appended sequentially ([0,0], [1,1], [2,2]...). Delete operations are batched in two patterns: backspace where position decrements after each deletion ([3,3], [2,2], [1,1]...), or forward delete where position stays the same ([0,0], [0,0]...).
When a user presses Cmd/Ctrl+Z or calls api.document.undo(), DocumentAPI dispatches a UndoCoreEvent. CollaborationManager listens first and immediately calls preventDefault() to take over undo/redo handling, forwarding the operation through the OT server (if connected). Core's UndoRedoManager also listens but only processes undo if not prevented by an earlier handler.
Rapid edits are merged into one logical batch; undo/redo replay inverses of that effective operation.