@terrencecrowley/ot-js
Version:
Javascript OT library
1,169 lines (1,060 loc) • 33.6 kB
text/typescript
import * as OT from "./ottypes";
const TestUnitSize: number = 4;
let TestCounter: number = 0;
// Array Ops
export const OpInsert: number = 1;
export const OpDelete: number = 2;
export const OpRetain: number = 3;
export const OpCursor: number = 4; // 2nd arg is 0/1 for start/end of region, 3rd arg is clientID
export const OpSet: number = 5;
export const OpTmpRetain: number = 6;
// Op, Len, Data
export type OTSingleArrayEdit = [number, number, any];
export type OTEdits = OTSingleArrayEdit[];
enum OTalignEdgesType { AlignForCompose, AlignForTransform };
// Set of interfaces that operates on the underlying array-like value type.
// If more efficient, can modify the initial argument and simply return it.
// Alternatively, can create new instance and return it (string works this way since
// underlying type is immutable).
// This is used for the array-like specialization of IOTInterface
export interface IOTArrayLikeOperations
{
underlyingTypeName(): string;
// Return empty instance
empty(): any;
// Insert new instance at position specified
insert(t: any, pos: number, tInsert: any): any; // Can modify input
// Delete part of instance
delete(t: any, pos: number, len: number): any; // Can modify input
// Set value of part of instance
set(t: any, pos: number, tSet: any): any; // Can modify input
// append to instance (insert at end)
append(t: any, tAppend: any): any; // Can modify input
// Take substring of instance
substr(t: any, pos: number, len: number): any; // Can modify input
// Take substring of provided base value type and set
substrOf(t: any, pos: number, len: number, tsub: any): any; // Cannot modify input
// Construct an instance N long
constructN(n: number): any;
// Test for equality
equal(t1: any, t2: any): boolean;
// copy an instance
copy(t: any): any; // Need to do deep copy if T is mutable.
// Return length of instance
length(t: any): number;
};
// Operates on a single "OTSingleArrayEdit", parameterized by an object that manipulates the underlying
// array-like value stored as the third property of the 3-element edit object.
export class OTSingleArrayEditor
{
raw: IOTArrayLikeOperations;
constructor(raw: IOTArrayLikeOperations)
{
this.raw = raw;
}
copy(a: OTSingleArrayEdit): OTSingleArrayEdit // If a[2] is mutable, need to override and do deep copy
{ return [ a[0], a[1], this.raw.copy(a[2]) ]; }
// Static Predicates for MoveAction
isDelete(a: OTSingleArrayEdit): boolean { return a[0] == OpDelete; }
isNotDelete(a: OTSingleArrayEdit): boolean { return a[0] != OpDelete; }
isCursor(a: OTSingleArrayEdit): boolean { return a[0] == OpCursor; }
isNotCursor(a: OTSingleArrayEdit): boolean { return a[0] != OpCursor; }
isTmpRetain(a: OTSingleArrayEdit): boolean { return a[0] == OpTmpRetain; }
isNotTmpRetainOrDelete(a: OTSingleArrayEdit): boolean { return (a[0] != OpTmpRetain && a[0] != OpDelete); }
isTmpRetainOrDelete(a: OTSingleArrayEdit): boolean { return (a[0] == OpTmpRetain || a[0] == OpDelete); }
// Other static predicates
isIgnore(a: OTSingleArrayEdit): boolean { return a[0] < 0; }
isNoOp(a: OTSingleArrayEdit): boolean { return a[1] === 0 && a[0] != OpCursor; }
isEqual(a1: OTSingleArrayEdit, a2: OTSingleArrayEdit) { return a1[0] == a2[0] && a1[1] == a2[1] && this.raw.equal(a1[2], a2[2]); }
// Helpers
appendValue(a: OTSingleArrayEdit, s: any): void
{ a[2] = this.raw.append(a[2], s); a[1] = a[1] + this.raw.length(s); }
empty(a: OTSingleArrayEdit): void { a[0] = OpCursor; a[1] = 0; a[2] = this.raw.empty(); }
setIgnore(a: OTSingleArrayEdit): void { if (a[0] > 0) a[0] = - a[0]; }
substr(aIn: OTSingleArrayEdit, pos: number, len: number): void
{
let sSource: any = aIn[2];
if (len > 0 && pos+len <= this.raw.length(sSource))
aIn[2] = this.raw.substr(sSource, pos, len);
aIn[1] = len;
}
substrFromRaw(aIn: OTSingleArrayEdit, pos: number, len: number, s: any): void
{
let sSource: any = s;
if (len > 0 && pos+len <= this.raw.length(sSource))
aIn[2] = this.raw.substr(sSource, pos, len);
aIn[1] = len;
}
copyWithSubstr(aIn: OTSingleArrayEdit, pos: number, len: number): OTSingleArrayEdit
{
let aOut: OTSingleArrayEdit = this.copy(aIn);
this.substr(aOut, pos, len);
return aOut;
}
};
export class OTStringOperations implements IOTArrayLikeOperations
{
underlyingTypeName(): string { return 'string'; }
empty(): any { return ''; }
insert(a: any, pos: number, aInsert: any): any
{
let s: string = a as string;
let sInsert: string = aInsert as string;
return s.substr(0, pos) + sInsert + s.substr(pos);
}
delete(a: any, pos: number, len: number): any
{
let s: string = a as string;
return s.substr(0, pos) + s.substr(pos+len);
}
set(a: any, pos: number, aSet: any): any
{
let s: string = a as string;
let sSet: string = aSet as string;
return s.substr(0, pos) + sSet + s.substr(pos+sSet.length);
}
append(a: any, aAppend: any): any
{
let s: string = a as string;
let sAppend: string = aAppend as string;
return s + sAppend;
}
substr(a: any, pos: number, len: number): any // Can modify source
{
let s: string = a as string;
return s.substr(pos, len);
}
substrOf(a: any, pos: number, len: number, aSub: any): any // Copies from tsub argument
{
// a unused if not updated with return value contents
let sSub: string = aSub as string;
return sSub.substr(pos, len);
}
constructN(n: number): any
{
let x = ' ';
let s = '';
for (;;)
{
if (n & 1)
s += x;
n >>= 1;
if (n)
x += x;
else
break;
}
return s;
}
equal(a1: any, a2: any): boolean
{
let s1: string = a1 as string;
let s2: string = a2 as string;
return s1 === s2;
}
copy(a: any): any { return a; }
length(a: any): number { return a.length; }
};
export class OTArrayOperations implements IOTArrayLikeOperations
{
underlyingTypeName(): string { return 'array'; }
empty(): any { return []; }
insert(a: any, pos: number, aInsert: any): any
{
let arr: Array<any> = a as Array<any>;
let arrInsert: Array<any> = aInsert as Array<any>;
let arrReturn = Array(arr.length + arrInsert.length);
let i: number, j: number;
for (i = 0; i < pos; i++)
arrReturn[i] = arr[i];
for (j = 0; j < arrInsert.length; j++)
arrReturn[i+j] = arrInsert[j];
for (i = pos; i < arr.length; i++)
arrReturn[i+j] = arr[i];
return arrReturn;
}
delete(a: any, pos: number, len: number): any
{
let arr: Array<any> = a as Array<any>;
arr.splice(pos, len);
return arr;
}
set(a: any, pos: number, aSet: any): any
{
let arr: Array<any> = a as Array<any>;
let arrSet: Array<any> = aSet as Array<any>;
for (let i: number = 0; i < arrSet.length; i++)
arr[i+pos] = arrSet[i];
return arr;
}
append(a: any, aAppend: any): any
{
let arr: Array<any> = a as Array<any>;
let arrAppend: Array<any> = aAppend as Array<any>;
return arr.concat(arrAppend);
}
substr(a: any, pos: number, len: number): any // Can modify source
{
let arr: Array<any> = a as Array<any>;
return arr.slice(pos, pos+len);
}
substrOf(a: any, pos: number, len: number, aSub: any): any // Copies from tsub argument
{
// a unused if not updated with return value contents
let arrSub: Array<any> = aSub as Array<any>;
return arrSub.slice(pos, pos+len);
}
constructN(n: number): any
{
return new Array(n);
}
equal(a1: any, a2: any): boolean
{
let arr1: Array<any> = a1 as Array<any>;
let arr2: Array<any> = a2 as Array<any>;
if (arr1.length != arr2.length)
return false;
for (let i: number = 0; i < arr1.length; i++)
if (arr1[i] !== arr2[i])
return false;
return true;
}
copy(a: any): any
{
let arr: Array<any> = a as Array<any>;
let arrRet = new Array(arr.length);
for (let i: number = 0; i < arr.length; i++)
arrRet[i] = arr[i];
return arrRet;
}
length(a: any): number
{
return a.length;
}
};
export class OTArrayLikeResource extends OT.OTResourceBase
{
editor: OTSingleArrayEditor;
constructor(ed: OTSingleArrayEditor, rname: string)
{
super(rname, ed.raw.underlyingTypeName());
this.editor = ed;
}
copy(): OTArrayLikeResource
{
return null; // Needs to be overridden
}
moveEdits(newA: OTEdits, iStart: number, iEnd?: number, pred?: (a: OTSingleArrayEdit) => boolean)
{
if (iEnd == undefined)
iEnd = this.edits.length - 1;
for (; iStart <= iEnd; iStart++)
{
let a: OTSingleArrayEdit = this.edits[iStart];
if (!this.editor.isIgnore(a) && (pred == undefined || pred(a)))
newA.push(a);
}
}
equal(rhs: OTArrayLikeResource): boolean
{
if (this.length != rhs.length)
return false;
for (let i: number = 0; i < this.length; i++)
if (! this.editor.isEqual(this.edits[i], rhs.edits[i]))
return false;
return true;
}
// Function: OTArrayLikeResource::effectivelyEqual
//
// Description:
// A looser definition than operator==. Returns true if two actions would result in the
// same final string. This ignores no-ops like OpCursor and allows different orderings of
// inserts and deletes at the same location.
//
// Played around with different algorithms, but the simplest is probably just to apply
// the two actions and see if I get the same final string. Came up with an interesting
// algorithm of walking through comparing hashes, but that was not robust to operations
// being split into fragments and interposed with alternate ops (OpCursor or interleaving of Ins/Del)
// that still leave the string the same. If unhappy with this approach (which scales with size
// of string to edit rather than complexity of the edit), the other approach would be to canonicalize
// the edit operations (including removing cursor operations and normalizing order of deletes).
// (Added that version of the algorithm under #ifdef). Could also dynamically choose approach based
// on relative size of arrays.
//
effectivelyEqual(rhs: OTArrayLikeResource): boolean
{
// Exactly equal is always effectively equal
if (this.equal(rhs))
return true;
if (this.originalLength() != rhs.originalLength())
return false;
// Preferred algorithm
let s: any = this.editor.raw.constructN(this.originalLength());
let sL: any = this.apply(s);
let sR: any = rhs.apply(s);
return sL === sR;
// Alternate algorithm (see above)
//let aL: OTArrayLikeResource = this.copy();
//let aR: OTArrayLikeResource = rhs.copy();
//aL.fullyCoalesce();
//aR.fullyCoalesce();
//return aL.equal(aR);
}
basesConsistent(rhs: OTArrayLikeResource): void
{
if (this.originalLength() != rhs.originalLength())
{
console.log("Logic Failure: transform: Bases Inconsistent.");
throw("Logic Failure: transform: Bases Inconsistent.");
}
}
originalLength(): number
{
let len: number = 0;
for (let i: number = 0; i < this.length; i++)
{
let a: OTSingleArrayEdit = this.edits[i];
if (a[0] == OpRetain || a[0] == OpDelete || a[0] == OpSet)
len += a[1];
}
return len;
}
finalLength(): number
{
let len: number = 0;
for (let i: number = 0; i < this.length; i++)
{
let a: OTSingleArrayEdit = this.edits[i];
if (a[0] == OpRetain || a[0] == OpInsert || a[0] == OpSet)
len += a[1];
}
return len;
}
apply(aValue: any): any
{
if (aValue == null)
aValue = this.editor.raw.empty();
let pos: number = 0;
for (let i: number = 0; i < this.length; i++)
{
let a: OTSingleArrayEdit = this.edits[i];
switch (a[0])
{
case OpRetain:
pos += a[1];
break;
case OpCursor:
break;
case OpDelete:
aValue = this.editor.raw.delete(aValue, pos, a[1]);
break;
case OpInsert:
aValue = this.editor.raw.insert(aValue, pos, a[2]);
pos += a[1];
break;
case OpSet:
aValue = this.editor.raw.set(aValue, pos, a[2]);
pos += a[1];
break;
}
}
return aValue;
}
coalesce(bDeleteCursor: boolean = false): void
{
if (this.length == 0)
return;
// coalesce adjoining actions and delete no-ops
let newA: OTEdits = [];
let aLast: OTSingleArrayEdit;
for (let i: number = 0; i < this.length; i++)
{
let aNext: OTSingleArrayEdit = this.edits[i];
if (this.editor.isNoOp(aNext) || (bDeleteCursor && aNext[0] == OpCursor))
continue;
if (newA.length > 0 && aNext[0] == aLast[0])
{
if (aNext[0] == OpInsert || aNext[0] == OpSet)
this.editor.appendValue(aLast, aNext[2]);
else
aLast[1] += aNext[1];
}
else
{
newA.push(aNext);
aLast = aNext;
}
}
this.edits = newA;
}
// Function: fullyCoalesce
//
// Description:
// Heavier duty version of coalesce that fully normalizes so that two actions that result in same
// final edit are exactly the same. This normalizes order of insert/deletes and deletes OpCursor,
// and then does coalesce.
//
fullyCoalesce(): void
{
// TODO
this.coalesce(true);
}
// Function: Invert
//
// Description:
// Given an action, convert it to its inverse (action + inverse) = identity (retain(n)).
//
// Note that in order to compute the inverse, you need the input state (e.g. because in order to invert
// OpDelete, you need to know the deleted characters.
//
invert(sInput: any): void
{
let pos: number = 0; // Tracks position in input string.
for (let i: number = 0; i < this.length; i++)
{
let a: OTSingleArrayEdit = this.edits[i];
switch (a[0])
{
case OpCursor:
break;
case OpRetain:
pos += a[1];
break;
case OpInsert:
a[2] = '';
a[0] = OpDelete;
break;
case OpDelete:
a[2] = this.editor.copyWithSubstr(sInput, pos, a[1]);
a[0] = OpInsert;
pos += a[1];
break;
case OpSet:
a[2] = this.editor.copyWithSubstr(sInput, pos, a[1]);
pos += a[1];
break;
}
}
}
// Function: alignEdges
//
// Description:
// Slice up this action sequence so its edges align with the action sequence I am going to
// process it with. The processing (compose or transform) determines which actions Slice
// takes into account when moving the parallel counters forward. When processing for
// compose, deletes in rhs can be ignored. When processing for transform, inserts in both
// lhs and rhs can be ignored.
//
alignEdges(rhs: OTArrayLikeResource, st: OTalignEdgesType): void
{
let posR: number = 0;
let posL: number = 0;
let iL: number = 0;
let newA: OTEdits = [];
let aAfter: OTSingleArrayEdit = undefined;
let aL: OTSingleArrayEdit = undefined;
for (let iR: number = 0; iR < rhs.length; iR++)
{
let aR: OTSingleArrayEdit = rhs.edits[iR];
switch (aR[0])
{
case OpCursor:
break;
case OpInsert:
break;
case OpDelete:
posR += aR[1];
break;
case OpSet:
posR += aR[1];
break;
case OpRetain:
posR += aR[1];
break;
}
// Advance iL/posL to equal to posR
while (posL < posR && (aAfter != undefined || iL < this.length))
{
if (aAfter == undefined)
{
aL = this.edits[iL];
newA.push(aL);
iL++;
}
else
{
aL = aAfter;
}
switch (aL[0])
{
case OpCursor:
break;
case OpInsert:
if (st == OTalignEdgesType.AlignForCompose) posL += aL[1];
break;
case OpDelete:
if (st == OTalignEdgesType.AlignForTransform) posL += aL[1];
break;
case OpSet:
posL += aL[1];
break;
case OpRetain:
posL += aL[1];
break;
}
// Split this one if it spans boundary
if (posL > posR)
{
let nRight: number = posL - posR;
let nLeft: number = aL[1] - nRight;
aAfter = this.editor.copyWithSubstr(aL, nLeft, nRight);
this.editor.substr(aL, 0, nLeft);
newA.push(aAfter);
posL = posR;
}
else
aAfter = undefined;
}
}
// Append any we missed
this.moveEdits(newA, iL);
this.edits = newA;
}
getCursorCache(): any
{
let cursorCache: any = { };
for (let i: number = 0; i < this.length; i++)
{
let a: OTSingleArrayEdit = this.edits[i];
if (a[0] == OpCursor && a[2] != null)
cursorCache[a[2]] = '';
}
return cursorCache;
}
// Function: compose
//
// Description:
// compose the current action with the action passed in. This alters the current action.
//
// Basic structure is to walk through the RHS list of actions, processing each one in turn.
// That then drives the walk through the left hand side and the necessary edits. I use
// "posR" and "posL" to work through equivalent positions in the two strings being edited.
// Deletions in the LHS don't effect posL because they don't show up in the input string to RHS.
// Similarly, insertions in the RHS don't effect posR since they have no equivalent string location
// in the LHS. (Note transform follows similar structure but different logic for how posR and posL
// track each other since in that case they are effectively referencing the same input string.)
//
compose(rhs: OTArrayLikeResource): void
{
let cursorCache: any = rhs.getCursorCache();
if (this.length == 0)
{
this.edits = rhs.edits.map(this.editor.copy, this.editor);
return;
}
else if (rhs.edits.length == 0)
return;
if (this.finalLength() != rhs.originalLength())
{
console.log("Logic Failure: compose: Bases Inconsistent.");
throw("Logic Failure: compose: Bases Inconsistent.");
}
// Break overlapping segments before start to simplify logic below.
this.alignEdges(rhs, OTalignEdgesType.AlignForCompose);
// Iterate with parallel position markers in two arrays
let posR: number = 0;
let posL: number = 0;
let iL: number = 0;
let bDone: boolean;
let newA: OTSingleArrayEdit[] = [];
for (let iR: number = 0; iR < rhs.length; iR++)
{
let aR: OTSingleArrayEdit = rhs.edits[iR];
switch (aR[0])
{
case OpRetain:
posR += aR[1];
break;
case OpSet:
case OpDelete:
case OpInsert:
case OpCursor:
// Advance to cursor location
bDone = false;
while (!bDone && iL < this.length)
{
let aL: OTSingleArrayEdit = this.edits[iL];
switch (aL[0])
{
case OpCursor:
// Only copy old cursor locations if they aren't empty and aren't duplicated in this rhs.
if (aL[2] != '' && cursorCache[aL[2]] === undefined)
newA.push(aL);
iL++;
break;
case OpSet:
case OpRetain:
case OpInsert:
if (posL == posR)
bDone = true;
else
{
posL += aL[1];
newA.push(aL);
iL++;
}
break;
case OpDelete:
newA.push(aL);
iL++; // Move past since deletes are not referenced by RHS
break;
}
}
if (aR[0] == OpDelete)
{
// Remove sequence of cursor, insert, retains, sets, replaced by delete.
// Note that insert/delete cancel each other out, so there is a bit of complexity there.
let nChange: number = aR[1];
let nRemain: number = aR[1];
for (; nChange > 0 && iL < this.length; iL++)
{
let aL: OTSingleArrayEdit = this.edits[iL];
switch (aL[0])
{
case OpCursor:
// Only copy old cursor locations if they aren't empty and aren't duplicated in this rhs.
if (aL[2] != '' && cursorCache[aL[2]] === undefined)
newA.push(aL);
break;
case OpDelete:
newA.push(aL);
break;
case OpSet:
case OpRetain:
case OpInsert:
nRemain -= aL[0] == OpInsert ? aL[1] : 0;
nChange -= aL[1];
// Don't copy into new array
break;
}
}
// Now add in the delete
if (nRemain > 0)
newA.push([ OpDelete, nRemain, '' ]);
}
else if (aR[0] == OpSet)
{
// Process sequence of cursor, insert, retains, sets
let nChange: number = aR[1];
for (; nChange > 0 && iL < this.length; iL++)
{
let aL: OTSingleArrayEdit = this.edits[iL];
let opL: number = OpInsert;
switch (aL[0])
{
case OpCursor:
// Only copy old cursor locations if they aren't empty and aren't duplicated in this rhs.
if (aL[2] != '' && cursorCache[aL[2]] === undefined)
newA.push(aL);
break;
case OpDelete:
newA.push(aL);
break;
case OpSet:
case OpRetain:
opL = OpSet;
// fallthrough
case OpInsert:
// A Set composed with Insert becomes Insert of Set content
this.editor.substrFromRaw(aL, aR[1]-nChange, aL[1], aR[2]);
aL[0] = opL;
nChange -= aL[1];
newA.push(aL);
break;
}
}
}
else // cursor, insert
{
// Add in the RHS operation at proper location
newA.push(this.editor.copy(aR));
}
break;
}
}
// copy any remaining actions, excluding cursors duplicated in rhs
this.moveEdits(newA, iL, this.length-1,
function (e: OTSingleArrayEdit)
{ return (e[0] != OpCursor) || (e[2] != '' && cursorCache[e[2]] === undefined) } );
this.edits = newA;
this.coalesce();
}
performTransformReorder(bForceRetainBeforeInsert: boolean, newA: OTEdits, iBegin: number, iEnd: number): void
{
if (iBegin < 0 || iBegin > iEnd) return;
if (bForceRetainBeforeInsert)
{
this.moveEdits(newA, iBegin, iEnd, this.editor.isTmpRetainOrDelete);
this.moveEdits(newA, iBegin, iEnd, this.editor.isNotTmpRetainOrDelete); // Is Insert or Cursor
}
else
{
this.moveEdits(newA, iBegin, iEnd, this.editor.isNotTmpRetainOrDelete); // Is Insert or Cursor
this.moveEdits(newA, iBegin, iEnd, this.editor.isTmpRetainOrDelete);
}
}
// Function: normalizeNewRetainsAfterTransform
//
// Description:
// Helper function for transform() that does a post-processing pass to ensure that all
// Retains are properly ordered with respect to Inserts that occur at the same location
// (either before or after, depending on whether we are transforming based on server or client side).
// This ensures that the transform process is not sensitive to precise ordering of Inserts and
// Retains (since that ordering doesn't actually change the semantics of the edit performed and
// therefore should not result in a difference in processing here). And yes, it's a subtle issue
// that may not actually occur in real edits produced by some particular editor but does arise when
// testing against randomly generated edit streams.
//
// A side consequence is also normalizing the ordering of inserts and deletes which also doesn't
// change the semantics of the edit but ensures we properly detect conflicting insertions.
//
// The way to think of this algorithm is that Set's and Retains (pre-existing, not new TmpRetains) form
// hard boundaries in the ordering. The series of Cursor/TmpRetain/Insert/Deletes between Sets and Retains
// are re-ordered by this algorithm. TmpRetain's get pushed to the front or the back depending on the bForce
// flag passed in (which reflects which operation had precedence).
//
normalizeNewRetainsAfterTransform(bForceRetainBeforeInsert: boolean): void
{
if (this.length == 0)
return;
let i: number = 0;
let newA: OTSingleArrayEdit[] = [];
let iLastEdge: number = 0;
// Normalize ordering for newly insert retains so they are properly ordered
// with respect to inserts occurring at the same location.
for (i = 0; i < this.length; i++)
{
let a: OTSingleArrayEdit = this.edits[i];
if (a[0] == OpSet || a[0] == OpRetain)
{
this.performTransformReorder(bForceRetainBeforeInsert, newA, iLastEdge, i-1);
newA.push(a);
iLastEdge = i+1;
}
}
this.performTransformReorder(bForceRetainBeforeInsert, newA, iLastEdge, this.length-1);
// One last time to switch TmpRetain to Retain
for (i = 0; i < newA.length; i++)
if ((newA[i])[0] == OpTmpRetain)
(newA[i])[0] = OpRetain;
this.edits = newA;
}
transform(prior: OTArrayLikeResource, bPriorIsService: boolean): void
{
if (this.length == 0 || prior.length == 0)
return;
// Validate
this.basesConsistent(prior);
// Break overlapping segments before start to simplify logic below.
this.alignEdges(prior, OTalignEdgesType.AlignForTransform);
let posR: number = 0; // These walk in parallel across the consistent base strings (only retains, sets and deletes count)
let posL: number = 0;
let iL: number = 0;
let bDone: boolean;
let newA: OTEdits = [];
for (let iR: number = 0; iR < prior.length; iR++)
{
let aR: OTSingleArrayEdit = prior.edits[iR];
switch (aR[0])
{
case OpCursor:
// No-op
break;
case OpInsert:
{
// Converts to a retain.
// Need to find spot to insert retain. After loop, iL will contain location
for (; iL < this.length; iL++)
{
if (posR == posL)
break;
let aL: OTSingleArrayEdit = this.edits[iL];
if (this.editor.isIgnore(aL))
continue;
if (aL[0] != OpCursor && aL[0] != OpInsert)
posL += aL[1];
newA.push(aL);
}
let nRetain: number = aR[1];
newA.push([ OpTmpRetain, nRetain, '' ]);
posR += nRetain;
posL += nRetain;
}
break;
case OpSet:
// Somewhat unintuitively, if prior is *not* service, then it will actually get applied *after*
// the service instance of OpSet and so should take precedence. Therefore if prior is not service,
// we need to go through and convert "OpSets" that overlap to be this content. If prior is service,
// we can just treat them as "retains" since they have no effect on our operations.
if (bPriorIsService)
posR += aR[1];
else
{
let nRemaining: number = aR[1];
while (nRemaining > 0 && iL < this.length)
{
let aL: OTSingleArrayEdit = this.edits[iL];
if (this.editor.isIgnore(aL))
{
iL++;
continue;
}
let valL: number = aL[1];
if (aL[0] == OpCursor || aL[0] == OpInsert)
{
iL++;
newA.push(aL);
}
else
{
if (posR >= posL+valL)
{
// Not there yet
posL += valL;
iL++;
newA.push(aL);
}
else
{
if (aL[0] == OpDelete || aL[0] == OpRetain)
{
if (valL <= nRemaining)
{
posR += valL;
posL += valL;
nRemaining -= valL;
iL++;
newA.push(aL);
}
else
{
// Not subsumed, but means that I didn't encounter an OpSet
posR += nRemaining;
nRemaining = 0;
}
}
else // OpSet
{
if (aL[1] <= nRemaining)
{
posR += valL;
posL += valL;
this.editor.substrFromRaw(aL, aR[1] - nRemaining, valL, aR[2]);
nRemaining -= valL;
iL++;
newA.push(aL);
}
else
{
// don't advance posL or iL because we will re-process the left over
// part for the next action. Simply edit the data in place.
// Set [0, nRemaining] of aL.Data to [aR[1]-nRemaining, nRemaining]
//aL.Data.delete(0, nRemaining);
//aL.Data.InsertValue(0, aR.Data, aR[1]-nRemaining, nRemaining);
aL[2] = aR[2].substr(aR[1] - nRemaining) + aL[2].substr(nRemaining);
posR += nRemaining;
nRemaining = 0;
}
}
}
}
}
}
break;
case OpDelete:
{
let nRemaining: number = aR[1];
let nDelay: number = 0;
let iDelay: number;
// Retains, sets and deletes are subsumed by prior deletes
for (; nRemaining > 0 && iL < this.length; iL++)
{
let aL: OTSingleArrayEdit = this.edits[iL];
if (this.editor.isIgnore(aL))
{
if (nDelay > 0)
nDelay++;
continue;
}
if (aL[0] == OpCursor || aL[0] == OpInsert)
{
if (nDelay == 0)
iDelay = iL;
nDelay++;
}
else
{
if (posR >= posL+aL[1])
{
// Go ahead and push any delayed actions
for (let j: number = iDelay; nDelay > 0; nDelay--, j++)
{
let aD: OTSingleArrayEdit = this.edits[j];
if (! this.editor.isIgnore(aD))
newA.push(aD);
}
// Prior to the deleted content
posL += aL[1];
newA.push(aL);
}
else
{
// Retain/set/delete is fully subsumed.
posR += aL[1];
posL += aL[1];
nRemaining -= aL[1];
this.editor.setIgnore(aL);
if (nDelay > 0)
nDelay++;
}
}
}
// We want to reprocess any trailing insert/cursors so we recognize conflicting inserts even when
// deletes intervene.
if (nDelay > 0)
iL = iDelay;
}
break;
case OpRetain:
// Just advance cursor
posR += aR[1];
break;
}
}
this.moveEdits(newA, iL);
this.edits = newA;
this.normalizeNewRetainsAfterTransform(bPriorIsService);
this.coalesce();
}
//
// Function: generateRandom
//
// Description:
// Generate action containing a sequence of retain, insert, delete, cursor with the initial
// state of the string being nInitial. Make sure I always generate at least one insert or delete.
// Always operate in units of 4 (.123).
//
generateRandom(nInitial: number, clientID: string): void
{
// Ensure clean start
this.empty();
// Setup randomizer
let nOps: number = 0;
let nLen: number;
let nBound: number;
let s: any;
while (nInitial > 0 || nOps == 0)
{
let op: number = randomWithinRange(0, 4);
nBound = nInitial / TestUnitSize;
if (nInitial == 0 && (op == OpDelete || op == OpRetain || op == OpSet))
continue;
switch (op)
{
case OpInsert:
nOps++;
nLen = randomWithinRange(1, 5);
s = this.editor.raw.empty();
for (let i: number = 0; i < nLen; i++)
s = this.editor.raw.append(s, counterValue(this.editor.raw, TestCounter++));
nLen *= TestUnitSize;
this.edits.push([ OpInsert, nLen, s ]);
break;
case OpDelete:
nOps++;
nLen = randomWithinRange(1, nBound > 3 ? nBound / 3 : nBound);
nLen *= TestUnitSize;
nInitial -= nLen;
this.edits.push([ OpDelete, nLen, this.editor.raw.empty() ]);
break;
case OpCursor:
this.edits.push([ OpCursor, 0, clientID ]);
break;
case OpRetain:
nLen = randomWithinRange(1, nBound);
nLen *= TestUnitSize;
nInitial -= nLen;
this.edits.push([ OpRetain, nLen, this.editor.raw.empty() ]);
break;
case OpSet:
nLen = 1;
s = this.editor.raw.empty();
for (let i: number = 0; i < nLen; i++)
this.editor.raw.append(s, counterValue(this.editor.raw, TestCounter++));
nLen *= TestUnitSize;
nInitial -= nLen;
this.edits.push([ OpSet, nLen, s ]);
break;
}
}
// Most importantly ensures canonical ordering of inserts and deletes.
this.coalesce();
}
}
export class OTStringResource extends OTArrayLikeResource
{
static _editor: OTSingleArrayEditor = new OTSingleArrayEditor(new OTStringOperations());
constructor(rname: string)
{
super(OTStringResource._editor, rname);
}
static factory(rname: string): OTStringResource { return new OTStringResource(rname); }
copy(): OTStringResource
{
let copy: OTStringResource = new OTStringResource(this.resourceName);
copy.edits = this.edits.map(copy.editor.copy, copy.editor);
return copy;
}
}
export class OTArrayResource extends OTArrayLikeResource
{
static _editor: OTSingleArrayEditor = new OTSingleArrayEditor(new OTArrayOperations());
constructor(rname: string)
{
super(OTArrayResource._editor, rname);
}
static factory(rname: string): OTArrayResource { return new OTArrayResource(rname); }
copy(): OTArrayResource
{
let copy: OTArrayResource = new OTArrayResource(this.resourceName);
copy.edits = this.edits.map(copy.editor.copy, copy.editor);
return copy;
}
}
function randomWithinRange(nMin: number, nMax: number): number
{
return nMin + Math.floor(Math.random() * (nMax - nMin + 1));
}
function counterValue(ops: IOTArrayLikeOperations, c: number): any
{
switch (ops.underlyingTypeName())
{
case 'string':
{
let a: string[] = new Array(TestUnitSize);
a[0] = '.';
for (let j: number = 1; j < TestUnitSize; j++, c = Math.floor(c / 10))
a[TestUnitSize - j] = "" + (c % 10);
return a.join('');
}
case 'array':
{
let a: number[] = new Array(TestUnitSize);
for (let i: number = 0; i < TestUnitSize; i++, c += 0.1)
a[i] = c;
return a;
}
default:
throw "counterValue: Unexpected underlying array-like type."
}
}