/** * 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._onDelta = null; this.editorAdapter.onDelta((delta) => { this.__handleClientDelta(delta); // This delta comes from the editor, so it has already been applied. this.__emitDelta(delta); }); this.serverAdapter.onDelta((delta) => { this.__handleServerDelta(delta); }); this.serverAdapter.onAcknowledgement(() => { this.__handleServerAcknowledgement(); }); } /** * Registers a callback called with a every delta applied to the editor. * * These deltas are already transformed such that applying them * one by one should eventually lead to the same state as on the server. */ onDelta(callback) { this._onDelta = callback; } __emitDelta(delta) { this._onDelta && this._onDelta(delta); } __handleClientDelta(delta) { this.state = this.state.onClientDelta(delta); } __handleServerDelta(delta) { this.revision++; this.state = this.state.onServerDelta(delta); } __handleServerAcknowledgement() { this.revision++; this.state = this.state.onServerAcknowledgement(); } applyDelta(delta) { this.editorAdapter.applyDelta(delta); // This delta comes from the server and we have just applied it to the editor. this.__emitDelta(delta); } sendDelta(delta) { this.serverAdapter.sendDelta(delta, this.revision + 1); } reportCurrentRevision() { this.serverAdapter.reportRevision(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, reportRevisionTimeout = 5000) { this.client = client; this.reportRevisionTimeoutId = null; this.reportRevisionTimeout = reportRevisionTimeout; } onClientDelta(delta) { // Cancel the report request if scheduled, // as the client is about to send the revision // along with own delta. if (this.reportRevisionTimeoutId !== null) { clearTimeout(this.reportRevisionTimeoutId); this.reportRevisionTimeoutId = null; } this.client.sendDelta(delta); return new AwaitingAcknowledgement(this.client, delta); } onServerDelta(delta) { this.client.applyDelta(delta); // The client received a new delta, so let's schedule // a request to report the new revision. if (this.reportRevisionTimeoutId === null) { this.reportRevisionTimeoutId = setTimeout(() => { this.client.reportCurrentRevision(); this.reportRevisionTimeoutId = null; }, this.reportRevisionTimeout); } 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 AwaitingAcknowledgement { 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 AwaitingAcknowledgement(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 AwaitingAcknowledgement(this.client, this.buffer); } }