@terrencecrowley/ot-js
Version:
Javascript OT library
1,206 lines (1,192 loc) • 84.7 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["ot-js"] = factory();
else
root["ot-js"] = factory();
})(global, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./lib/all.ts");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./lib/all.ts":
/*!********************!*\
!*** ./lib/all.ts ***!
\********************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
Object.defineProperty(exports, "__esModule", { value: true });
__export(__webpack_require__(/*! ./otarray */ "./lib/otarray.ts"));
__export(__webpack_require__(/*! ./otengine */ "./lib/otengine.ts"));
__export(__webpack_require__(/*! ./otclientengine */ "./lib/otclientengine.ts"));
__export(__webpack_require__(/*! ./otcomposite */ "./lib/otcomposite.ts"));
__export(__webpack_require__(/*! ./otmap */ "./lib/otmap.ts"));
__export(__webpack_require__(/*! ./otcounter */ "./lib/otcounter.ts"));
__export(__webpack_require__(/*! ./otserverengine */ "./lib/otserverengine.ts"));
__export(__webpack_require__(/*! ./ottypes */ "./lib/ottypes.ts"));
__export(__webpack_require__(/*! ./otsession */ "./lib/otsession.ts"));
/***/ }),
/***/ "./lib/otarray.ts":
/*!************************!*\
!*** ./lib/otarray.ts ***!
\************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const OT = __webpack_require__(/*! ./ottypes */ "./lib/ottypes.ts");
const TestUnitSize = 4;
let TestCounter = 0;
// Array Ops
exports.OpInsert = 1;
exports.OpDelete = 2;
exports.OpRetain = 3;
exports.OpCursor = 4; // 2nd arg is 0/1 for start/end of region, 3rd arg is clientID
exports.OpSet = 5;
exports.OpTmpRetain = 6;
var OTalignEdgesType;
(function (OTalignEdgesType) {
OTalignEdgesType[OTalignEdgesType["AlignForCompose"] = 0] = "AlignForCompose";
OTalignEdgesType[OTalignEdgesType["AlignForTransform"] = 1] = "AlignForTransform";
})(OTalignEdgesType || (OTalignEdgesType = {}));
;
;
// 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.
class OTSingleArrayEditor {
constructor(raw) {
this.raw = raw;
}
copy(a) { return [a[0], a[1], this.raw.copy(a[2])]; }
// Static Predicates for MoveAction
isDelete(a) { return a[0] == exports.OpDelete; }
isNotDelete(a) { return a[0] != exports.OpDelete; }
isCursor(a) { return a[0] == exports.OpCursor; }
isNotCursor(a) { return a[0] != exports.OpCursor; }
isTmpRetain(a) { return a[0] == exports.OpTmpRetain; }
isNotTmpRetainOrDelete(a) { return (a[0] != exports.OpTmpRetain && a[0] != exports.OpDelete); }
isTmpRetainOrDelete(a) { return (a[0] == exports.OpTmpRetain || a[0] == exports.OpDelete); }
// Other static predicates
isIgnore(a) { return a[0] < 0; }
isNoOp(a) { return a[1] === 0 && a[0] != exports.OpCursor; }
isEqual(a1, a2) { return a1[0] == a2[0] && a1[1] == a2[1] && this.raw.equal(a1[2], a2[2]); }
// Helpers
appendValue(a, s) { a[2] = this.raw.append(a[2], s); a[1] = a[1] + this.raw.length(s); }
empty(a) { a[0] = exports.OpCursor; a[1] = 0; a[2] = this.raw.empty(); }
setIgnore(a) { if (a[0] > 0)
a[0] = -a[0]; }
substr(aIn, pos, len) {
let sSource = aIn[2];
if (len > 0 && pos + len <= this.raw.length(sSource))
aIn[2] = this.raw.substr(sSource, pos, len);
aIn[1] = len;
}
substrFromRaw(aIn, pos, len, s) {
let sSource = s;
if (len > 0 && pos + len <= this.raw.length(sSource))
aIn[2] = this.raw.substr(sSource, pos, len);
aIn[1] = len;
}
copyWithSubstr(aIn, pos, len) {
let aOut = this.copy(aIn);
this.substr(aOut, pos, len);
return aOut;
}
}
exports.OTSingleArrayEditor = OTSingleArrayEditor;
;
class OTStringOperations {
underlyingTypeName() { return 'string'; }
empty() { return ''; }
insert(a, pos, aInsert) {
let s = a;
let sInsert = aInsert;
return s.substr(0, pos) + sInsert + s.substr(pos);
}
delete(a, pos, len) {
let s = a;
return s.substr(0, pos) + s.substr(pos + len);
}
set(a, pos, aSet) {
let s = a;
let sSet = aSet;
return s.substr(0, pos) + sSet + s.substr(pos + sSet.length);
}
append(a, aAppend) {
let s = a;
let sAppend = aAppend;
return s + sAppend;
}
substr(a, pos, len) {
let s = a;
return s.substr(pos, len);
}
substrOf(a, pos, len, aSub) {
// a unused if not updated with return value contents
let sSub = aSub;
return sSub.substr(pos, len);
}
constructN(n) {
let x = ' ';
let s = '';
for (;;) {
if (n & 1)
s += x;
n >>= 1;
if (n)
x += x;
else
break;
}
return s;
}
equal(a1, a2) {
let s1 = a1;
let s2 = a2;
return s1 === s2;
}
copy(a) { return a; }
length(a) { return a.length; }
}
exports.OTStringOperations = OTStringOperations;
;
class OTArrayOperations {
underlyingTypeName() { return 'array'; }
empty() { return []; }
insert(a, pos, aInsert) {
let arr = a;
let arrInsert = aInsert;
let arrReturn = Array(arr.length + arrInsert.length);
let i, j;
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, pos, len) {
let arr = a;
arr.splice(pos, len);
return arr;
}
set(a, pos, aSet) {
let arr = a;
let arrSet = aSet;
for (let i = 0; i < arrSet.length; i++)
arr[i + pos] = arrSet[i];
return arr;
}
append(a, aAppend) {
let arr = a;
let arrAppend = aAppend;
return arr.concat(arrAppend);
}
substr(a, pos, len) {
let arr = a;
return arr.slice(pos, pos + len);
}
substrOf(a, pos, len, aSub) {
// a unused if not updated with return value contents
let arrSub = aSub;
return arrSub.slice(pos, pos + len);
}
constructN(n) {
return new Array(n);
}
equal(a1, a2) {
let arr1 = a1;
let arr2 = a2;
if (arr1.length != arr2.length)
return false;
for (let i = 0; i < arr1.length; i++)
if (arr1[i] !== arr2[i])
return false;
return true;
}
copy(a) {
let arr = a;
let arrRet = new Array(arr.length);
for (let i = 0; i < arr.length; i++)
arrRet[i] = arr[i];
return arrRet;
}
length(a) {
return a.length;
}
}
exports.OTArrayOperations = OTArrayOperations;
;
class OTArrayLikeResource extends OT.OTResourceBase {
constructor(ed, rname) {
super(rname, ed.raw.underlyingTypeName());
this.editor = ed;
}
copy() {
return null; // Needs to be overridden
}
moveEdits(newA, iStart, iEnd, pred) {
if (iEnd == undefined)
iEnd = this.edits.length - 1;
for (; iStart <= iEnd; iStart++) {
let a = this.edits[iStart];
if (!this.editor.isIgnore(a) && (pred == undefined || pred(a)))
newA.push(a);
}
}
equal(rhs) {
if (this.length != rhs.length)
return false;
for (let i = 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) {
// Exactly equal is always effectively equal
if (this.equal(rhs))
return true;
if (this.originalLength() != rhs.originalLength())
return false;
// Preferred algorithm
let s = this.editor.raw.constructN(this.originalLength());
let sL = this.apply(s);
let sR = 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) {
if (this.originalLength() != rhs.originalLength()) {
console.log("Logic Failure: transform: Bases Inconsistent.");
throw ("Logic Failure: transform: Bases Inconsistent.");
}
}
originalLength() {
let len = 0;
for (let i = 0; i < this.length; i++) {
let a = this.edits[i];
if (a[0] == exports.OpRetain || a[0] == exports.OpDelete || a[0] == exports.OpSet)
len += a[1];
}
return len;
}
finalLength() {
let len = 0;
for (let i = 0; i < this.length; i++) {
let a = this.edits[i];
if (a[0] == exports.OpRetain || a[0] == exports.OpInsert || a[0] == exports.OpSet)
len += a[1];
}
return len;
}
apply(aValue) {
if (aValue == null)
aValue = this.editor.raw.empty();
let pos = 0;
for (let i = 0; i < this.length; i++) {
let a = this.edits[i];
switch (a[0]) {
case exports.OpRetain:
pos += a[1];
break;
case exports.OpCursor:
break;
case exports.OpDelete:
aValue = this.editor.raw.delete(aValue, pos, a[1]);
break;
case exports.OpInsert:
aValue = this.editor.raw.insert(aValue, pos, a[2]);
pos += a[1];
break;
case exports.OpSet:
aValue = this.editor.raw.set(aValue, pos, a[2]);
pos += a[1];
break;
}
}
return aValue;
}
coalesce(bDeleteCursor = false) {
if (this.length == 0)
return;
// coalesce adjoining actions and delete no-ops
let newA = [];
let aLast;
for (let i = 0; i < this.length; i++) {
let aNext = this.edits[i];
if (this.editor.isNoOp(aNext) || (bDeleteCursor && aNext[0] == exports.OpCursor))
continue;
if (newA.length > 0 && aNext[0] == aLast[0]) {
if (aNext[0] == exports.OpInsert || aNext[0] == exports.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() {
// 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) {
let pos = 0; // Tracks position in input string.
for (let i = 0; i < this.length; i++) {
let a = this.edits[i];
switch (a[0]) {
case exports.OpCursor:
break;
case exports.OpRetain:
pos += a[1];
break;
case exports.OpInsert:
a[2] = '';
a[0] = exports.OpDelete;
break;
case exports.OpDelete:
a[2] = this.editor.copyWithSubstr(sInput, pos, a[1]);
a[0] = exports.OpInsert;
pos += a[1];
break;
case exports.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, st) {
let posR = 0;
let posL = 0;
let iL = 0;
let newA = [];
let aAfter = undefined;
let aL = undefined;
for (let iR = 0; iR < rhs.length; iR++) {
let aR = rhs.edits[iR];
switch (aR[0]) {
case exports.OpCursor:
break;
case exports.OpInsert:
break;
case exports.OpDelete:
posR += aR[1];
break;
case exports.OpSet:
posR += aR[1];
break;
case exports.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 exports.OpCursor:
break;
case exports.OpInsert:
if (st == OTalignEdgesType.AlignForCompose)
posL += aL[1];
break;
case exports.OpDelete:
if (st == OTalignEdgesType.AlignForTransform)
posL += aL[1];
break;
case exports.OpSet:
posL += aL[1];
break;
case exports.OpRetain:
posL += aL[1];
break;
}
// Split this one if it spans boundary
if (posL > posR) {
let nRight = posL - posR;
let nLeft = 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() {
let cursorCache = {};
for (let i = 0; i < this.length; i++) {
let a = this.edits[i];
if (a[0] == exports.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) {
let cursorCache = 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 = 0;
let posL = 0;
let iL = 0;
let bDone;
let newA = [];
for (let iR = 0; iR < rhs.length; iR++) {
let aR = rhs.edits[iR];
switch (aR[0]) {
case exports.OpRetain:
posR += aR[1];
break;
case exports.OpSet:
case exports.OpDelete:
case exports.OpInsert:
case exports.OpCursor:
// Advance to cursor location
bDone = false;
while (!bDone && iL < this.length) {
let aL = this.edits[iL];
switch (aL[0]) {
case exports.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 exports.OpSet:
case exports.OpRetain:
case exports.OpInsert:
if (posL == posR)
bDone = true;
else {
posL += aL[1];
newA.push(aL);
iL++;
}
break;
case exports.OpDelete:
newA.push(aL);
iL++; // Move past since deletes are not referenced by RHS
break;
}
}
if (aR[0] == exports.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 = aR[1];
let nRemain = aR[1];
for (; nChange > 0 && iL < this.length; iL++) {
let aL = this.edits[iL];
switch (aL[0]) {
case exports.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 exports.OpDelete:
newA.push(aL);
break;
case exports.OpSet:
case exports.OpRetain:
case exports.OpInsert:
nRemain -= aL[0] == exports.OpInsert ? aL[1] : 0;
nChange -= aL[1];
// Don't copy into new array
break;
}
}
// Now add in the delete
if (nRemain > 0)
newA.push([exports.OpDelete, nRemain, '']);
}
else if (aR[0] == exports.OpSet) {
// Process sequence of cursor, insert, retains, sets
let nChange = aR[1];
for (; nChange > 0 && iL < this.length; iL++) {
let aL = this.edits[iL];
let opL = exports.OpInsert;
switch (aL[0]) {
case exports.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 exports.OpDelete:
newA.push(aL);
break;
case exports.OpSet:
case exports.OpRetain:
opL = exports.OpSet;
// fallthrough
case exports.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) { return (e[0] != exports.OpCursor) || (e[2] != '' && cursorCache[e[2]] === undefined); });
this.edits = newA;
this.coalesce();
}
performTransformReorder(bForceRetainBeforeInsert, newA, iBegin, iEnd) {
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) {
if (this.length == 0)
return;
let i = 0;
let newA = [];
let iLastEdge = 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 = this.edits[i];
if (a[0] == exports.OpSet || a[0] == exports.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] == exports.OpTmpRetain)
(newA[i])[0] = exports.OpRetain;
this.edits = newA;
}
transform(prior, bPriorIsService) {
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 = 0; // These walk in parallel across the consistent base strings (only retains, sets and deletes count)
let posL = 0;
let iL = 0;
let bDone;
let newA = [];
for (let iR = 0; iR < prior.length; iR++) {
let aR = prior.edits[iR];
switch (aR[0]) {
case exports.OpCursor:
// No-op
break;
case exports.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 = this.edits[iL];
if (this.editor.isIgnore(aL))
continue;
if (aL[0] != exports.OpCursor && aL[0] != exports.OpInsert)
posL += aL[1];
newA.push(aL);
}
let nRetain = aR[1];
newA.push([exports.OpTmpRetain, nRetain, '']);
posR += nRetain;
posL += nRetain;
}
break;
case exports.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 = aR[1];
while (nRemaining > 0 && iL < this.length) {
let aL = this.edits[iL];
if (this.editor.isIgnore(aL)) {
iL++;
continue;
}
let valL = aL[1];
if (aL[0] == exports.OpCursor || aL[0] == exports.OpInsert) {
iL++;
newA.push(aL);
}
else {
if (posR >= posL + valL) {
// Not there yet
posL += valL;
iL++;
newA.push(aL);
}
else {
if (aL[0] == exports.OpDelete || aL[0] == exports.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 exports.OpDelete:
{
let nRemaining = aR[1];
let nDelay = 0;
let iDelay;
// Retains, sets and deletes are subsumed by prior deletes
for (; nRemaining > 0 && iL < this.length; iL++) {
let aL = this.edits[iL];
if (this.editor.isIgnore(aL)) {
if (nDelay > 0)
nDelay++;
continue;
}
if (aL[0] == exports.OpCursor || aL[0] == exports.OpInsert) {
if (nDelay == 0)
iDelay = iL;
nDelay++;
}
else {
if (posR >= posL + aL[1]) {
// Go ahead and push any delayed actions
for (let j = iDelay; nDelay > 0; nDelay--, j++) {
let aD = 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 exports.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, clientID) {
// Ensure clean start
this.empty();
// Setup randomizer
let nOps = 0;
let nLen;
let nBound;
let s;
while (nInitial > 0 || nOps == 0) {
let op = randomWithinRange(0, 4);
nBound = nInitial / TestUnitSize;
if (nInitial == 0 && (op == exports.OpDelete || op == exports.OpRetain || op == exports.OpSet))
continue;
switch (op) {
case exports.OpInsert:
nOps++;
nLen = randomWithinRange(1, 5);
s = this.editor.raw.empty();
for (let i = 0; i < nLen; i++)
s = this.editor.raw.append(s, counterValue(this.editor.raw, TestCounter++));
nLen *= TestUnitSize;
this.edits.push([exports.OpInsert, nLen, s]);
break;
case exports.OpDelete:
nOps++;
nLen = randomWithinRange(1, nBound > 3 ? nBound / 3 : nBound);
nLen *= TestUnitSize;
nInitial -= nLen;
this.edits.push([exports.OpDelete, nLen, this.editor.raw.empty()]);
break;
case exports.OpCursor:
this.edits.push([exports.OpCursor, 0, clientID]);
break;
case exports.OpRetain:
nLen = randomWithinRange(1, nBound);
nLen *= TestUnitSize;
nInitial -= nLen;
this.edits.push([exports.OpRetain, nLen, this.editor.raw.empty()]);
break;
case exports.OpSet:
nLen = 1;
s = this.editor.raw.empty();
for (let i = 0; i < nLen; i++)
this.editor.raw.append(s, counterValue(this.editor.raw, TestCounter++));
nLen *= TestUnitSize;
nInitial -= nLen;
this.edits.push([exports.OpSet, nLen, s]);
break;
}
}
// Most importantly ensures canonical ordering of inserts and deletes.
this.coalesce();
}
}
exports.OTArrayLikeResource = OTArrayLikeResource;
class OTStringResource extends OTArrayLikeResource {
constructor(rname) {
super(OTStringResource._editor, rname);
}
static factory(rname) { return new OTStringResource(rname); }
copy() {
let copy = new OTStringResource(this.resourceName);
copy.edits = this.edits.map(copy.editor.copy, copy.editor);
return copy;
}
}
OTStringResource._editor = new OTSingleArrayEditor(new OTStringOperations());
exports.OTStringResource = OTStringResource;
class OTArrayResource extends OTArrayLikeResource {
constructor(rname) {
super(OTArrayResource._editor, rname);
}
static factory(rname) { return new OTArrayResource(rname); }
copy() {
let copy = new OTArrayResource(this.resourceName);
copy.edits = this.edits.map(copy.editor.copy, copy.editor);
return copy;
}
}
OTArrayResource._editor = new OTSingleArrayEditor(new OTArrayOperations());
exports.OTArrayResource = OTArrayResource;
function randomWithinRange(nMin, nMax) {
return nMin + Math.floor(Math.random() * (nMax - nMin + 1));
}
function counterValue(ops, c) {
switch (ops.underlyingTypeName()) {
case 'string':
{
let a = new Array(TestUnitSize);
a[0] = '.';
for (let j = 1; j < TestUnitSize; j++, c = Math.floor(c / 10))
a[TestUnitSize - j] = "" + (c % 10);
return a.join('');
}
case 'array':
{
let a = new Array(TestUnitSize);
for (let i = 0; i < TestUnitSize; i++, c += 0.1)
a[i] = c;
return a;
}
default:
throw "counterValue: Unexpected underlying array-like type.";
}
}
/***/ }),
/***/ "./lib/otclientengine.ts":
/*!*******************************!*\
!*** ./lib/otclientengine.ts ***!
\*******************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const OTC = __webpack_require__(/*! ./otcomposite */ "./lib/otcomposite.ts");
const OTE = __webpack_require__(/*! ./otengine */ "./lib/otengine.ts");
class OTClientEngine extends OTE.OTEngine {
// Constructor
constructor(ilog, rid, cid) {
super(ilog);
this.resourceID = rid;
this.clientID = cid;
this.initialize();
this.bReadOnly = false;
this.valCache = {};
}
initialize() {
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() {
return this.stateServer.clock;
}
rid() {
return this.resourceID;
}
cid() {
return this.resourceID;
}
toValue() {
return this.valCache;
}
setReadOnly(b) {
if (b != this.bReadOnly) {
this.bReadOnly = b;
if (this.bReadOnly)
this.failbackToServerState();
}
}
startLocalEdit() {
return new OTC.OTCompositeResource(this.resourceID, this.clientID);
}
isPending() {
return this.isNeedResend || !this.actionAllPendingClient.isEmpty();
}
getPending() {
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() {
if (this.isNeedAck) {
this.isNeedAck = false;
this.isNeedResend = true;
}
}
// When I don't accurately have server state - will then refresh from server
failbackToInitialState() {
this.initialize();
}
// When I have server state but my state got mixed up
failbackToServerState() {
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) {
// 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;
}