livebook/assets/js/editor/editor_client.js
Jonatan Kłosko 3e6a4adce2
Implement collaborative text editing (#10)
* Set up editor and client side delta handling

* Synchronize deltas on the server

* Document the client code, add more tests

* Implement delta on the server, use compact representation when transmitting changes

* Simplify transformation implementation and add documentation

* Add session and data tests

* Add more delta tests

* Clean up evaluator tests wait timeout
2021-01-21 13:11:45 +01:00

155 lines
4.5 KiB
JavaScript

/**
* A manager associated with a particular editor instance,
* which is responsible for controlling client-server communication
* and synchronizing the sent/received changes.
*
* This class takes `serverAdapter` and `editorAdapter` objects
* that encapsulate the logic relevant for each part.
*
* ## Changes synchronization
*
* When the local editor emits a change (represented as delta),
* the client sends this delta to the server and waits for an acknowledgement.
* Until the acknowledgement comes, the client keeps all further
* edits in buffer.
* The server may send either an acknowledgement or other client's delta.
* It's important to note that those messages come in what the server
* believes is chronological order, so any delta received before
* the acknowledgement should be treated as if it happened before
* our unacknowledged delta.
* Other client's delta is transformed against the local unacknowledged
* deltas and applied to the editor.
*/
export default class EditorClient {
constructor(serverAdapter, editorAdapter, revision) {
this.serverAdapter = serverAdapter;
this.editorAdapter = editorAdapter;
this.revision = revision;
this.state = new Synchronized(this);
this.editorAdapter.onDelta((delta) => {
this.__handleClientDelta(delta);
});
this.serverAdapter.onDelta((delta) => {
this.__handleServerDelta(delta);
});
this.serverAdapter.onAcknowledgement(() => {
this.__handleServerAcknowledgement();
});
}
__handleClientDelta(delta) {
this.state = this.state.onClientDelta(delta);
}
__handleServerDelta(delta) {
this.revision++;
this.state = this.state.onServerDelta(delta);
}
__handleServerAcknowledgement() {
this.state = this.state.onServerAcknowledgement();
}
applyDelta(delta) {
this.editorAdapter.applyDelta(delta);
}
sendDelta(delta) {
this.revision++;
this.serverAdapter.sendDelta(delta, this.revision);
}
}
/**
* Client is in this state when there is no delta pending acknowledgement
* (the client is fully in sync with the server).
*/
class Synchronized {
constructor(client) {
this.client = client;
}
onClientDelta(delta) {
this.client.sendDelta(delta);
return new AwaitingConfirm(this.client, delta);
}
onServerDelta(delta) {
this.client.applyDelta(delta);
return this;
}
onServerAcknowledgement() {
throw new Error("Unexpected server acknowledgement.");
}
}
/**
* Client is in this state when the client sent one delta and waits
* for an acknowledgement, while there are no other deltas in a buffer.
*/
class AwaitingConfirm {
constructor(client, awaitedDelta) {
this.client = client;
this.awaitedDelta = awaitedDelta;
}
onClientDelta(delta) {
return new AwaitingWithBuffer(this.client, this.awaitedDelta, delta);
}
onServerDelta(delta) {
// We consider the incoming delta to happen first
// (because that's the case from the server's perspective).
const deltaPrime = this.awaitedDelta.transform(delta, "right");
this.client.applyDelta(deltaPrime);
const awaitedDeltaPrime = delta.transform(this.awaitedDelta, "left");
return new AwaitingConfirm(this.client, awaitedDeltaPrime);
}
onServerAcknowledgement() {
return new Synchronized(this.client);
}
}
/**
* Client is in this state when the client sent one delta and waits
* for an acknowledgement, while there are more deltas in a buffer.
*/
class AwaitingWithBuffer {
constructor(client, awaitedDelta, buffer) {
this.client = client;
this.awaitedDelta = awaitedDelta;
this.buffer = buffer;
}
onClientDelta(delta) {
const newBuffer = this.buffer.compose(delta);
return new AwaitingWithBuffer(this.client, this.awaitedDelta, newBuffer);
}
onServerDelta(delta) {
// We consider the incoming delta to happen first
// (because that's the case from the server's perspective).
// Delta transformed against awaitedDelta
const deltaPrime = this.awaitedDelta.transform(delta, "right");
// Delta transformed against both awaitedDelta and the buffer (appropriate for applying to the editor)
const deltaBis = this.buffer.transform(deltaPrime, "right");
this.client.applyDelta(deltaBis);
const awaitedDeltaPrime = delta.transform(this.awaitedDelta, "left");
const bufferPrime = deltaPrime.transform(this.buffer, "left");
return new AwaitingWithBuffer(this.client, awaitedDeltaPrime, bufferPrime);
}
onServerAcknowledgement() {
this.client.sendDelta(this.buffer);
return new AwaitingConfirm(this.client, this.buffer);
}
}