A JavaScript implementation of the Operational Transformation algorithm that lets multiple clients edit the same in-cloud document at the same time.
Your code needs to handle the editor in the client, the communication layer between the client and the server and, optionally, the storage of the document in the server. This library handles the synchronization and coherence of all the documents' versions (server and clients). Therefore, your code needs to communicate with in on 7 occasions:
- In the client:
Client#operation: when you tell the engine that the document changed in the editor (e.g.: user input)Client#onMessage: when the engine tells you to send a message to the serverClient#message: when you tell the engine that a message has been received from the serverClient#onEvent: when the engine tells you that you need to change the document
- In the server:
ClientInterface#message: when you tell the engine that a message has been received from the clientClientInterface#onMessage: when the engine tells you to send a message to the clientServer#onEvent: when the engine tells you to update the DB (optional)
(NB: operation doesn't exist in the server because you can't write on the server's document directly, it needs to go through a client instance. A client instance may be hosted on the server, though.)
Document Client Server
editor instance instance Database
| ---> operation() >| | |
| |> onMessage() ----> message() >| |
| |< message() <---- onMessage() <|> onEvent() ----> |
| <----- onEvent() <| | |
A Component is one block of an Operation. It's either a retain, insert or delete Component.
Type of the Component: retain, insert or delete
An Operation is an object that represents an edition of a whole document.
Pushes a retain Component to the current Operation.
Pushes an insert Component to the current Operation.
Pushes a delete Component to the current Operation.
Takes any number of objects that describe components and pushes the corresponding Components to the current Operation
This method is equivalent to calling ret(), ins() and del() but may be more convenient depending on how you build Operations.
Array of Components representing the whole Operation
Operation unique identifier
ID of the Operation's author
ID of the parent Operation
A Message is an object that needs to be sent on the wire either from server to client or from client to server. It contains 2 properties:
metadata is a simple string. You don't need to understand what it contains and it needs to be kept as is in the object.
data is an array that contains individual items of your document (e.g. characters). If they need to be serialized or transformed, you can manipulate that array. On the other end of the wire, you need to properly deserialize them so that the other part of the engine is able to interpret them. If you're only using items that are supported by JSON, you can simplify JSON.stringify and JSON.parse the whole object: myTransport.send(JSON.stringify(message)).
These are the atomical elements that you provide to build the document. In most cases, for text based documents, they're simple characters ('a', 'b', 'c', ...) but for a more complex kind of document, they could be anything, as long as they can be handled by JavaScript. They aren't even necessarily the same type in the clients and the server, as long as you translate them consistently when the messages are sent between the client and the server. For instance, they could be DOM nodes in browser based clients, instances of a custom class in the server and something else in native apps. Your job is to handle the translation when messages are sent and to provide a areEqual function to compare them when necessary.
All the client's options can be assigned after its instanciation. e.g.:
const client = new Client()
// ...
client.onMessage = handleMessage
onEvent({ type: String, status: String, op: Operation?, document: Element[] })
The function that will be called when an event (either
resetoroperation) needs to be handled by the local editor.
-
type
reset: the client is being reset with a fresh version of the document. The editor should be assigned thedocumentparameter. -
type
operation: an Operation needs to be applied on the editor. The new document is also passed for convenience. -
onMessage(clientToServerMessage: Message)
The function that will be called when a message needs to be sent to the server.
When this function is called, send the message to the server and pass it to the ClientInterface#message method.
areEqual(c1: Component, c2: Component): Boolean
An optional function used to compare 2 elements of the Document
If your document contains elements that cannot be compared with ===, then you need to provide this comparison function. If you don't provide it, elements will be compared with a === equality.
The current version of the document.
The current status of the client
"detached": the client doesn't know anything about the server. This status requires to callreset()on the client."pending": the client is currently awaiting for an operation it sent earlier. In the mean time, the client can receive and apply other operations from the server and it can buffer other operations from the client."sync": the client is fully synchronized with the server.
Apply a new operation on top of the current state.
Pass a message coming from the server to the client's local engine.
Request the full document from the server. Also used to initialize.
This should trigger a reset event once the client has received the document from the server.
document
The initial document.
The document is an array of arbitrary types. The array items need to be serializable via JSON.stringify. If not passed, the server will be initialized with an empty document.
onEvent({ type: String, operation: Operation, document: Element[] })
Optionally subscribe to operations.
Will be called everytime that an operation has been applied. This is useful to keep a database synced. type will always be operation. The full document is also passed for convenience.
You can assign it later with: server.onEvent = handleEvent
areEqual(c1: Component, c2: Component): Boolean
Same as Client's areEqual
The current version of the document.
Returns a new
ClientInterface.
id
ID of the client
This ID will be used to identify Operations authors. If not provided, a random ID will be assigned to the client.
onMessage(serverToClientMessage: Message)
The function that will be called when a message needs to be sent to the client.
When this function is called, send the message to the client and pass it to the Client#message method.
You can assign it later with: clientInterface.onMessage = handleMessage
Pass a message coming from the client to the server's local engine.
const server = new Server({
document: db.getDocument() || [],
// Server -> Database
onEvent: event => {
if (event.type === 'operation') {
db.update(event.operation)
}
}
})
io.on('connection', socket => {
// Server -> Client
const clientInterface = server.addClient({
onMessage: message => {
socket.emit('serverToClientMessage', message)
}
})
// Client -> Server
socket.on('clientToServerMessage', message => {
clientInterface.message(message)
})
})const editor = new MyEditor()
socket.on('connection', () => {
const client = new Client({
// Client -> Editor
onEvent: event => {
if (event.type === 'reset') {
editor.setContent(event.document)
} else if (event.type === 'operation') {
editor.applyOperation(event.operation)
}
},
// Client -> Server
onMessage: message => {
socket.emit('clientToServerMessage', JSON.stringify(message))
}
})
// Editor -> Client
editor.on('keypress', (position, key) => {
const op = new Client.Operation()
if (isBackSpace(key)) {
op
.ret(position - 1)
.del(editor.slice(position - 1, position))
} else {
op
.ret(position)
.ins(key)
}
op.ret(editor.contentLength - position)
client.operation(op)
})
// Server -> Client
socket.on('serverToClientMessage', message => {
client.message(JSON.parse(message))
})
// Notify the server that the client is ready to receive events
client.reset()
})