UNPKG

@terrencecrowley/ot-js

Version:
1,169 lines (1,060 loc) 33.6 kB
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." } }