@terrencecrowley/ot-js
Version:
Javascript OT library
298 lines (253 loc) • 8.61 kB
text/typescript
// 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;
}
}