UNPKG

@terrencecrowley/ot-js

Version:
300 lines (266 loc) 9.24 kB
// Shared libraries import * as ILog from "@terrencecrowley/logabstract"; import * as OT from "./ottypes"; import * as OTC from "./otcomposite"; import * as OTE from "./otengine"; export class OTClientEngine extends OTE.OTEngine { // Data members clientID: string; resourceID: string; isNeedAck: boolean; isNeedResend: boolean; bReadOnly: boolean; clientSequenceNo: number; stateServer: OTC.OTCompositeResource; stateLocal: OTC.OTCompositeResource; valCache: any; actionAllClient: OTC.OTCompositeResource; actionAllPendingClient: OTC.OTCompositeResource; actionSentClient: OTC.OTCompositeResource; actionSentClientOriginal: OTC.OTCompositeResource; actionServerInterposedSentClient: OTC.OTCompositeResource; // Constructor constructor(ilog: ILog.ILog, rid: string, cid: string) { super(ilog); this.resourceID = rid; this.clientID = cid; this.initialize(); this.bReadOnly = false; this.valCache = {}; } initialize(): void { this.clientSequenceNo = 0; this.isNeedAck = false; this.isNeedResend = false; this.actionAllClient = new OTC.OTCompositeResource(this.resourceID, this.clientID); this.actionAllPendingClient = new OTC.OTCompositeResource(this.resourceID, this.clientID); this.actionSentClient = new OTC.OTCompositeResource(this.resourceID, this.clientID); this.actionSentClientOriginal = new OTC.OTCompositeResource(this.resourceID, this.clientID); this.actionServerInterposedSentClient = new OTC.OTCompositeResource(this.resourceID, this.clientID); this.stateServer = new OTC.OTCompositeResource(this.resourceID, this.clientID); this.stateLocal = new OTC.OTCompositeResource(this.resourceID, this.clientID); } // Members serverClock(): number { return this.stateServer.clock; } rid(): string { return this.resourceID; } cid(): string { return this.resourceID; } toValue(): any { return this.valCache; } setReadOnly(b: boolean): void { if (b != this.bReadOnly) { this.bReadOnly = b; if (this.bReadOnly) this.failbackToServerState(); } } startLocalEdit(): OTC.OTCompositeResource { return new OTC.OTCompositeResource(this.resourceID, this.clientID); } isPending(): boolean { return this.isNeedResend || !this.actionAllPendingClient.isEmpty(); } getPending(): OTC.OTCompositeResource { if (!this.isNeedResend && this.actionAllPendingClient.isEmpty()) return null; else { // If "isNeedResend" I need to send the exact same event (instead of aggregating all pending) // because the server might have actually received and processed the event and I just didn't // receive acknowledgement. If I merge that event into others I'll lose ability to distinguish // that. Eventually when I re-establish communication with server I will get that event response // and can then move on. if (! this.isNeedResend) { this.actionSentClient = this.actionAllPendingClient.copy(); this.actionSentClient.clientSequenceNo = this.clientSequenceNo++; this.actionAllPendingClient.empty(); } this.actionSentClient.clock = this.stateServer.clock; this.actionSentClientOriginal = this.actionSentClient.copy(); this.actionServerInterposedSentClient.empty(); this.isNeedAck = true; this.isNeedResend = false; return this.actionSentClient.copy(); } } // When I fail to send, I need to reset to resend the event again resetPending(): void { if (this.isNeedAck) { this.isNeedAck = false; this.isNeedResend = true; } } // When I don't accurately have server state - will then refresh from server failbackToInitialState(): void { this.initialize(); } // When I have server state but my state got mixed up failbackToServerState(): void { this.stateLocal = this.stateServer.copy(); this.isNeedAck = false; this.actionSentClient.empty(); this.actionSentClientOriginal.empty(); this.actionServerInterposedSentClient.empty(); this.actionAllPendingClient.empty(); this.actionAllClient.empty(); this.valCache = this.stateLocal.toValue(); this.emit('state'); } // // Function: OTClientEngine.addRemote // // Description: // This function is really where the action is in managing the dynamic logic of applying OT. This is run // on each end point and handles the events received from the server. This includes server acknowledgements // (both success and failure) of locally generated events as well as all the events generated from other // clients. // // The key things that happen here are: // 1. Track server state. // 2. Respond to server acknowledgement of locally generated events. This also includes validation // (with failback code) in case where server transformed my event in a way that was inconsistent // with what I expected (due to insert collision that arose due to multiple independent events). // 3. Transform the incoming event (by local events) so it can be applied to local state. // 4. Transform pending local events so they can be dispatched to the service once the service // is ready for another event. // addRemote(orig: OTC.OTCompositeResource): void { // Reset if server forces restart if (orig.clock == OTC.clockInitialValue) { this.failbackToInitialState(); return; } // Reset if server restarted and we don't sync up if (orig.clock < 0) { // If server didn't lose anything I can just keep going... if (this.stateServer.clock+1 == -orig.clock) orig.clock = - orig.clock else { this.failbackToInitialState(); return; } } // Ignore if I've seen this event already if (orig.clock <= this.serverClock()) { return; } let bMine: boolean = orig.clientID == this.clientID; let bResend: boolean = bMine && orig.clock == OTC.clockFailureValue; let a: OTC.OTCompositeResource = orig.copy(); if (bResend) { // Service failed my request. Retry with currently outstanding content. this.resetPending(); return; } try { // Track server state and clock this.stateServer.compose(a); if (bMine) { // Validate that I didn't run into unresolvable conflict if (! this.actionServerInterposedSentClient.isEmpty()) { this.actionSentClientOriginal.transform(this.actionServerInterposedSentClient, true); if (! this.actionSentClient.effectivelyEqual(this.actionSentClientOriginal)) { this.failbackToServerState(); } } // I don't need to apply to local state since it has already been applied - this is just an ack. this.isNeedAck = false; this.actionSentClient.empty(); this.actionSentClientOriginal.empty(); this.actionServerInterposedSentClient.empty(); this.actionAllClient = this.actionAllPendingClient.copy(); } else { // Transform server action to apply locally by transforming by all pending client actions a.transform(this.actionAllClient, false); // And then compose with local state this.stateLocal.compose(a); // Transform pending client by server action so it is rooted off the server state. // This ensures that I can convert the next server action I receive. this.actionAllClient.transform(orig, true); // Transform server action to be after previously sent client action and then // transform the unsent actions so they are ready to be sent. let aServerTransformed: OTC.OTCompositeResource = orig.copy(); aServerTransformed.transform(this.actionSentClient, false); this.actionAllPendingClient.transform(aServerTransformed, true); // And then transform the sent client action so ready to be used for transforming next server event this.actionSentClient.transform(orig, true); // Track server operations interposed between a sent action if (this.isNeedAck) this.actionServerInterposedSentClient.compose(orig); // Let clients know this.valCache = this.stateLocal.toValue(); this.emit('state'); } } catch (err) { this.ilog.error("OTClientEngine.addRemote: unexpected exception: " + err); this.failbackToInitialState(); } } // // Function: addLocalEdit // // Description: // This is the logic for adding an action to the local state. The logic is straight-forward // as we need to track: // 1. The composed set of unacknowledged locally generated events. // 2. The composed set of unsent locally generated events (queued until sent event is acknowledged). // 3. The local state. // 4. An undo operation. // addLocalEdit(orig: OTC.OTCompositeResource): void { if (! this.bReadOnly) { try { this.actionAllClient.compose(orig); this.actionAllPendingClient.compose(orig); this.stateLocal.compose(orig); this.valCache = this.stateLocal.toValue(); this.emit('state'); } catch (err) { this.ilog.error("OTClientEngine.addLocalEdit: unexpected exception: " + err); this.failbackToInitialState(); } } } };