UNPKG

@terrencecrowley/ot-js

Version:
298 lines (253 loc) 8.61 kB
// Shared libraries import * as ILog from "@terrencecrowley/logabstract"; // Local libraries import * as OT from "./ottypes"; import * as OTC from "./otcomposite"; import * as OTS from "./otsession"; import * as OTE from "./otengine"; export const ClientIDForServer: string = '-Server-'; export class OTServerEngine extends OTE.OTEngine { // Data members stateServer: OTC.OTCompositeResource; logServer: OTC.OTCompositeResource[]; valCache: any; highSequence: any; clientSequenceNo: number; // Constructor constructor(ilog: ILog.ILog, rid: string) { super(ilog); this.stateServer = new OTC.OTCompositeResource(rid, ""); this.logServer = []; this.highSequence = {}; this.clientSequenceNo = 0; this.valCache = {}; } serverClock(): number { return this.stateServer.clock; } rid(): string { return this.stateServer.resourceName; } cid(): string { return ClientIDForServer; } startLocalEdit(): OTC.OTCompositeResource { return new OTC.OTCompositeResource(this.rid(), this.cid()); } toValue(): any { return this.valCache; } getProp(s: string): any { let o: any = this.valCache['WellKnownName_meta']; return o === undefined ? '' : o[s]; } getName(): string { return this.getProp('name'); } getType(): string { return this.getProp('type'); } getDescription(): string { return this.getProp('description'); } getCreatedBy(): string { return this.getProp('createdby'); } getCreateTime(): string { return this.getProp('createtime'); } getCreatedByName(): string { let s: string = this.getCreatedBy(); if (s != '') { let users: any = this.valCache['WellKnownName_users']; if (users && users[s] && users[s]['name']) return users[s]['name']; } return ''; } hasSeenEvent(orig: OTC.OTCompositeResource): boolean { let clientSequenceNo: any = this.highSequence[orig.clientID]; let bSeen = (clientSequenceNo !== undefined && Number(clientSequenceNo) >= orig.clientSequenceNo); return bSeen; } isNextEvent(orig: OTC.OTCompositeResource): boolean { let nSeen: any = this.highSequence[orig.clientID]; let bNext = (nSeen === undefined && orig.clientSequenceNo == 0) || (Number(nSeen)+1 == orig.clientSequenceNo); if (! bNext) { if (nSeen === undefined) this.ilog.event(`session(${this.stateServer.resourceID}): non-zero client seqNo (${orig.clientSequenceNo}) for unseen client`); else this.ilog.event(`session(${this.stateServer.resourceID}): expected client seqNo ${Number(nSeen)+1} but saw ${orig.clientSequenceNo}`); } return bNext; } rememberSeenEvent(orig: OTC.OTCompositeResource): void { this.highSequence[orig.clientID] = orig.clientSequenceNo; } forgetEvents(orig: OTC.OTCompositeResource): void { delete this.highSequence[orig.clientID]; } clientHighSequence(cid: string): number { let clientSequenceNo: any = this.highSequence[cid]; return clientSequenceNo === undefined ? 0 : Number(clientSequenceNo); } garbageCollect(): void { if (this.stateServer.garbageCollect(this.valCache ? this.valCache['WellKnownName_resource'] : null)) { this.valCache = this.stateServer.toValue(); this.emit('state'); } // TODO: Also remove entries from log to minimize memory use. } // Function: addServer // // Description: // This is the server state update processing upon receiving an event from an endpoint. // The received event is transformed (if possible) and added to the server state. // The logic here is straight-forward - transform the incoming event so it is relative to // the current state and then apply. addServer(orig: OTC.OTCompositeResource): number { try { // First transform, then add to log let i: number; let a: OTC.OTCompositeResource = orig.copy(); for (i = this.logServer.length; i > 0; i--) { let aService: OTC.OTCompositeResource = this.logServer[i-1]; if (aService.clock == a.clock) break; } // Fail if we've seen it already (client did not receive ack) if (this.hasSeenEvent(orig)) { this.ilog.event({ sessionid: this.stateServer.resourceID, event: `addServer: received duplicate event.` }); this.forgetEvents(orig); // we are now resetting client in this case, so forget this client return OTS.EClockSeen; } // If this isn't next in sequence, I lost one (probably because I went "back in time" // due to server restart). In that case client is forced to re-initialize (losing local // edits). I also need to re-initialize sequence numbering. if (! this.isNextEvent(orig)) { this.ilog.event({ sessionid: this.stateServer.resourceID, event: `addServer: received out-of-order event` }); this.forgetEvents(orig); return OTS.EClockReset; } // Fail if we have discarded that old state if (a.clock >= 0 && i == 0) { this.ilog.event({ sessionid: this.stateServer.resourceID, event: `addServer: received old event` }); // This should really be ClockFailure which would force the client to resend with a newer // clock value. But there appears to be a bug when session is reloaded that results in // client never getting synced up. So for now force a reset (which might result in some // client edits being discarded). this.forgetEvents(orig); return OTS.EClockReset; //return OTS.EClockFailure; } // OK, all good, transform and apply if (i < this.logServer.length) { let aPrior: OTC.OTCompositeResource = this.logServer[i].copy(); for (i++; i < this.logServer.length; i++) aPrior.compose(this.logServer[i]); a.transform(aPrior, true); } a.clock = this.stateServer.clock + 1; this.stateServer.compose(a); this.valCache = this.stateServer.toValue(); this.emit('state'); this.logServer.push(a.copy()); this.rememberSeenEvent(orig); return OTS.ESuccess; } catch (err) { this.ilog.error('addServer: unexpected exception'); this.forgetEvents(orig); return OTS.EClockReset; //return OTS.EClockFailure; } } addLocalEdit(orig: OTC.OTCompositeResource): void { orig.clock = this.serverClock(); orig.clientSequenceNo = this.clientSequenceNo++; let errno: number = this.addServer(orig); } toJSON(): any { let log: any[] = []; for (let i: number = 0; i < this.logServer.length; i++) log.push(this.logServer[i].toJSON()); return { state: this.stateServer.toJSON(), highSequence: this.highSequence, log: log }; } validateLog(): void { // Yikes, invalid log created by bad revision reverting - validate on load and truncate if necessary try { if (this.logServer.length > 0) { let aPrior: OTC.OTCompositeResource = this.logServer[0].copy(); for (let i: number = 1; i < this.logServer.length; i++) aPrior.compose(this.logServer[i]); } } catch (err) { this.ilog.event({ sessionid: this.stateServer.resourceID, event: `OTServer: corrupted log truncated` }); this.logServer = []; this.logServer.push(this.stateServer.copy()); } } loadFromObject(o: any): void { if (o.state !== undefined) { this.stateServer = OTC.OTCompositeResource.constructFromObject(o.state); this.logServer = []; this.valCache = this.stateServer.toValue(); this.emit('state'); } if (o.log !== undefined) { for (let i: number = 0; i < o.log.length; i++) this.logServer.push(OTC.OTCompositeResource.constructFromObject(o.log[i])); this.validateLog(); } else { this.logServer = []; this.logServer.push(this.stateServer.copy()); } if (o.highSequence !== undefined) this.highSequence = o.highSequence; this.clientSequenceNo = this.clientHighSequence(ClientIDForServer) + 1; } }