diffusion
Version:
Diffusion JavaScript client
553 lines (552 loc) • 22 kB
JavaScript
"use strict";
/**
* @module diffusion.datatypes
*/
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MyersBinaryDiff = exports.coalesce = exports.NOOP = exports.MATCH = exports.INSERT = exports.Execution = exports.checkBounds = exports.corner = exports.nextR = exports.nextF = exports.setR = exports.setF = exports.getR = exports.getF = exports.keyR = exports.keyF = exports.Storage = exports.NO_CHANGE = exports.REPLACE = exports.SUCCESS = void 0;
/* eslint-disable jsdoc/require-jsdoc */
var errors_1 = require("./../../../errors/errors");
var math_1 = require("./../../util/math");
var BAIL_OUT_FACTOR = 10000;
var MAXIMUM_STORAGE = 0x7fffffff;
// Constant result codes
exports.SUCCESS = 0;
exports.REPLACE = 1;
exports.NO_CHANGE = 2;
var Storage = /** @class */ (function () {
function Storage(max) {
this.maximumD = (max - 3) / 4;
this.vectorLength = 0;
this.vector = [];
}
Storage.prototype.fill = function (start) {
for (var i = start; i < this.vectorLength + 4; i += 4) {
this.vector[i + 0] = -1;
this.vector[i + 1] = -1;
this.vector[i + 2] = 0x7fffffff;
this.vector[i + 3] = 0x7fffffff;
}
};
Storage.prototype.ensure = function (d) {
this.vectorLength = 4 * (d + 1) + 3;
};
Storage.prototype.initialise = function (d) {
this.ensure(d);
this.fill(3);
return this.vector;
};
Storage.prototype.extend = function (d) {
if (d > this.maximumD) {
return null;
}
var originalLength = this.vectorLength;
this.ensure(d);
this.fill(originalLength);
return this.vector;
};
return Storage;
}());
exports.Storage = Storage;
// Forward key
function keyF(k) {
return k < 0 ? -4 * k - 1 : 4 * k;
}
exports.keyF = keyF;
// Reverse key
function keyR(k) {
return keyF(k) + 2;
}
exports.keyR = keyR;
// Get forward
function getF(v, k) {
var i = v[keyF(k)];
/* tslint:disable-next-line:strict-type-predicates */
return i === undefined ? 0 : i;
}
exports.getF = getF;
// Get reverse
function getR(v, k) {
var i = v[keyR(k)];
/* tslint:disable-next-line:strict-type-predicates */
return i === undefined ? 0 : i;
}
exports.getR = getR;
// Set forward
function setF(v, k, i) {
v[keyF(k)] = i;
}
exports.setF = setF;
// Set reverse
function setR(v, k, i) {
var key = keyR(k);
v[key] = i;
}
exports.setR = setR;
// Next forward
function nextF(v, k) {
var left = getF(v, k + 1);
var right = getF(v, k - 1);
return left < right ? right : left + 1;
}
exports.nextF = nextF;
// Next reverse
function nextR(v, k) {
var left = getR(v, k + 1);
var right = getR(v, k - 1);
return left < right ? left : right - 1;
}
exports.nextR = nextR;
/**
* Limit a diagonal by the length of an input.
*
* If d is greater than the length, return the diagonal to use instead. This
* is used to constrain the forward vector entries to an (n+1) * (m+1)
* rectangle, and the reverse vector entries to an (m+1) * (n+1) rectangle.
*/
function corner(d, length) {
if (d <= length) {
return d;
}
else {
return 2 * length - d;
}
}
exports.corner = corner;
function checkBounds(buffer, offset, length) {
if (offset < 0) {
throw new errors_1.InternalError("offset " + offset + " < 0");
}
if (length < 0) {
throw new errors_1.InternalError("length " + length + " < 0");
}
if (offset + length > buffer.length || offset + length < 0) {
throw new errors_1.InternalError("offset " + offset + " + " + length + " > " + buffer.length);
}
}
exports.checkBounds = checkBounds;
var Execution = /** @class */ (function () {
function Execution(storage, a, b, script, bailOutFactor) {
this.storage = storage;
this.a = a;
this.b = b;
this.script = script;
this.bailOutFactor = bailOutFactor;
}
Execution.prototype.diff = function (aOffset, aLength, bOffset, bLength, limit) {
if (limit === void 0) { limit = this.bailOutFactor; }
checkBounds(this.a, aOffset, aLength);
checkBounds(this.b, bOffset, bLength);
// trim common prefix and suffix.
// middleSnake() relies on this to avoid searching the
// initial diagonal.
var p = this.afterCommonPrefix(aOffset, aLength, bOffset, bLength);
var u = this.beforeCommonSuffix(aOffset, aLength, bOffset, bLength, p);
var v = u + bLength - aLength;
// add the common prefix to the script
if (!this.script.match(aOffset, p) /* prefix */) {
return false;
}
var result;
if (p === u) {
result = this.script.insert(bOffset + p, v - p);
}
else if (p === v) {
result = this.script.delete();
}
else {
result = this.middleSnake(aOffset + p, u - p, bOffset + p, v - p, limit);
}
return result && this.script.match(aOffset + u, aLength - u); // Suffix
};
/**
* Search for the first mismatch between the buffers `a` and `b`.
*
* @param aoff the offset in buffer `a` to start from
* @param n the maximum number of bytes to search in `a`
* @param boff the offset in buffer `b` to start from
* @param m the maximum number of bytes to search in `b`
* @return the first position of the mismatch counted from the offsets
*/
Execution.prototype.afterCommonPrefix = function (aoff, n, boff, m) {
var end = Math.min(n, m);
for (var p = 0; p < end; ++p) {
if (this.a[aoff + p] !== this.b[boff + p]) {
return p;
}
}
return Math.max(0, end);
};
/**
* Search for the first mismatch between the buffers `a` and `b` starting
* from the back.
*
* @param aoff the offset in buffer `a` to start from
* @param n the maximum number of bytes to search in `a`
* @param boff the offset in buffer `b` to start from
* @param m the maximum number of bytes to search in `b`
* @return the first position of the mismatch counted from the offsets
*/
Execution.prototype.beforeCommonSuffix = function (aoff, aLength, boff, bLength, limit) {
var end = Math.max(0, aLength - bLength) + limit;
for (var u = aLength; u > end; --u) {
if (this.a[aoff + u - 1] !== this.b[boff + u + bLength - aLength - 1]) {
return u;
}
}
return Math.min(aLength, end);
};
/**
* Variant of {@link Execution.diff} used when there is known to be no common
* prefix.
*/
Execution.prototype.diffNoPrefix = function (aoff, aLength, boff, bLength, limit) {
if (limit === void 0) { limit = this.bailOutFactor; }
var u = this.beforeCommonSuffix(aoff, aLength, boff, bLength, 0);
var v = u + bLength - aLength;
var r;
if (0 === u) {
r = this.script.insert(boff, v);
}
else if (0 === v) {
r = this.script.delete();
}
else {
r = this.middleSnake(aoff, u, boff, v, limit);
}
return r && this.script.match(aoff + u, aLength - u); // suffix
};
/**
* Variant of {@link Execution.diff} used when there is known to be no common
* suffix.
*/
Execution.prototype.diffNoSuffix = function (aoff, aLength, boff, bLength, limit) {
if (limit === void 0) { limit = this.bailOutFactor; }
var p = this.afterCommonPrefix(aoff, aLength, boff, bLength);
if (!this.script.match(aoff, p) /* prefix */) {
// Possible if the edit script fails to return SUCCESS for the
// pending operation.
return false;
}
if (p === aLength) {
return this.script.insert(boff + p, bLength - p);
}
else if (p === bLength) {
return this.script.delete();
}
else {
return this.middleSnake(aoff + p, aLength - p, boff + p, bLength - p, limit);
}
};
/* eslint-disable-next-line complexity */
Execution.prototype.middleSnake = function (aOffset, aLength, bOffset, bLength, initialLimit) {
var delta = aLength - bLength;
/* eslint-disable-next-line no-bitwise */
var odd = delta & 1;
var vec = this.storage.initialise(1);
setF(vec, 0, 0);
setR(vec, 0, aLength);
var d = 1;
var limit = initialLimit;
do {
var cornerA = corner(d, aLength);
var cornerB = corner(d, bLength);
for (var k1 = -cornerA; k1 <= cornerB; k1 += 2) {
var x1 = nextF(vec, k1);
var u1 = x1 + this.afterCommonPrefix(aOffset + x1, aLength - x1, bOffset + x1 + k1, bLength - k1 - x1);
if (odd && Math.abs(k1 + delta) <= d - 1 && u1 >= getR(vec, k1 + delta)) {
return this.recurse(aOffset, aLength, bOffset, bLength, x1, u1, k1);
}
setF(vec, k1, u1);
}
for (var k2 = -cornerB; k2 <= cornerA; k2 += 2) {
var u2 = nextR(vec, k2);
var kd = k2 - delta;
var x2 = this.beforeCommonSuffix(aOffset, u2, bOffset, kd + u2, 0);
if (!odd && Math.abs(kd) <= d && x2 <= getF(vec, kd)) {
return this.recurse(aOffset, aLength, bOffset, bLength, x2, u2, kd);
}
setR(vec, k2, x2);
}
// Constant soup ahead. The following values were found to
// work well be experiment. The exact values are not too
// critical, but don't change them without measuring
// significant benefits in the (cost, delta size) and
// the bail out responsiveness.
// 1. We check progress every 64 diagonals ...
/* eslint-disable-next-line no-bitwise */
if ((d & 0x3f) === 0 && this.slowProgress(vec, aLength, bLength, cornerA, cornerB, d)) {
// 2 ... and on finding slow progress, reduce the limit.
// Periodic checking balances the cost of the check against
// earlier detection, and avoids progress checking for small
// search spaces.
// The reduction of the limit by a proportion the period is
// key to bailing out fast without collapsing. So the value
// it needs to be high enough, but not equal to the period.
limit -= 0x30;
}
else if (d > Math.max(4, limit)) {
// 3. We must iterate a minimum number of times to avoid the
// possibility of stack overflow through low-progress recursion.
return this.bail(vec, aOffset, aLength, bOffset, bLength, cornerA, cornerB, Math.trunc(limit / 2));
}
++d;
vec = this.storage.extend(d);
} while (vec !== null);
return false;
};
Execution.prototype.slowProgress = function (vec, aLength, bLength, cornerA, cornerB, d) {
var _a = __read(this.bestForward(vec, aLength, bLength, cornerA, cornerB), 2), xforward = _a[0], yforward = _a[1];
var _b = __read(this.bestReverse(vec, aLength, bLength, cornerA, cornerB), 2), xreverse = _b[0], yreverse = _b[1];
var fbest = xforward + yforward;
var rbest = aLength + bLength - xreverse - yreverse;
var progress = fbest + rbest;
// It's time for some serious hand waving.
// progress >= 2d.
// The cost of continuing an iteration is O(d^2).
// But this is a sampling approach and there may good progress to
// be made later. So let's pick a function that lies in between.
return progress < math_1.approximateSquareRoot(d) * d;
};
Execution.prototype.bestForward = function (vec, aLength, bLength, cornerA, cornerB) {
var xforward = 0;
var yforward = 0;
for (var k = -cornerA; k <= cornerB; k += 2) {
// Forward x values can exceed n because we don't constrain
// FORWARD.getNext(). Similarly y values can exceed n. Detect
// and slide back within the square.
var x = Math.min(getF(vec, k), aLength, bLength - k);
var y = x + k;
if (x + y > xforward + yforward) {
xforward = x;
yforward = y;
}
}
return [xforward, yforward];
};
Execution.prototype.bestReverse = function (vec, aLength, bLength, cornerA, cornerB) {
var xreverse = aLength;
var yreverse = bLength;
for (var k = -cornerB; k <= cornerA; k += 2) {
// Similarly reverse x and y values can be less than 0.
var kd = k - (aLength - bLength);
var x = Math.max(getR(vec, k), 0, -kd);
var y = x + kd;
if (x + y < xreverse + yreverse) {
xreverse = x;
yreverse = y;
}
}
return [xreverse, yreverse];
};
Execution.prototype.recurse = function (aOffset, aLength, bOffset, bLength, x, u, k) {
return this.diffNoPrefix(aOffset, x, bOffset, x + k)
&& this.script.match(aOffset + x, u - x)
&& this.diffNoSuffix(aOffset + u, aLength - u, bOffset + u + k, bLength - u - k);
};
Execution.prototype.bail = function (vec, aOffset, aLength, bOffset, bLength, cornerA, cornerB, limit) {
var _a = __read(this.bestForward(vec, aLength, bLength, cornerA, cornerB), 2), xforward = _a[0], yforward = _a[1];
var _b = __read(this.bestReverse(vec, aLength, bLength, cornerA, cornerB), 2), xreverse = _b[0], yreverse = _b[1];
if (xforward >= xreverse || yforward >= yreverse) {
// Best forward and reverse searches overlap, use the one
// with the most progress to split space into two parts.
if (aLength + bLength - xreverse - yreverse > xforward + yforward) {
return this.splitDiff(aOffset, xreverse, aLength, bOffset, yreverse, bLength);
}
else {
return this.splitDiff(aOffset, xforward, aLength, bOffset, yforward, bLength);
}
}
else {
// Best forward and reverse searches don't overlap. Split
// into three parts.
return this.diffNoPrefix(aOffset, xforward, bOffset, yforward)
// Use boundedDiff for the unexplored territory.
&& this.boundedDiff(aOffset + xforward, xreverse - xforward, bOffset + yforward, yreverse - yforward, aLength, bLength, limit)
&& this.diffNoSuffix(aOffset + xreverse, aLength - xreverse, bOffset + yreverse, bLength - yreverse);
}
};
/*
* Variant of #diff used by #bail to ensure we are dividing the problem space
* sufficiently to prevent stack overflow
*/
Execution.prototype.boundedDiff = function (aOffset, aLength, bOffset, bLength, totalN, totalM, limit) {
var totalSpace = totalN * totalM;
var nm = aLength * bLength;
// We use Math.floor here instead of a bitwise op as totalSpace may be an integer larger than 32 bits
/* eslint-disable-next-line no-bitwise */
var threshold = Math.floor((1 << 24) + totalSpace / 2);
if (nm >= threshold) {
if (limit <= 0) {
// OK, let's *really* slam the breaks on.
// Don't do any further analysis on the first and last
// quarter of the unexplored space.
var n0 = Math.trunc(aLength / 4);
var n1 = n0 + Math.trunc(aLength / 2);
var m0 = Math.trunc(bLength / 4);
var m1 = m0 + Math.trunc(bLength / 2);
return this.noDiff(bOffset, m0) &&
this.diff(aOffset + n0, n1 - n0, bOffset + m0, m1 - m0, limit) &&
this.noDiff(bOffset + m1, bLength - m1);
}
var x = Math.trunc(aLength / 2);
var y = Math.trunc(bLength / 2);
return this.splitDiff(aOffset, x, aLength, bOffset, y, bLength, limit);
}
else {
return this.middleSnake(aOffset, aLength, bOffset, bLength, limit);
}
};
Execution.prototype.splitDiff = function (aOffset, n1, n2, bOffset, m1, m2, limit) {
return this.diffNoPrefix(aOffset, n1, bOffset, m1, limit)
&& this.diffNoSuffix(aOffset + n1, n2 - n1, bOffset + m1, m2 - m1, limit);
};
Execution.prototype.noDiff = function (bOffset, bLength) {
return this.script.insert(bOffset, bLength) && this.script.delete();
};
return Execution;
}());
exports.Execution = Execution;
// Diff operations
function INSERT(script, start, length) {
return script.insert(start, length);
}
exports.INSERT = INSERT;
function MATCH(script, start, length) {
return script.match(start, length);
}
exports.MATCH = MATCH;
function NOOP() {
return true;
}
exports.NOOP = NOOP;
function coalesce(delegate, aOffset, bOffset) {
var neverFlushed = true;
var pendingLength = 0;
var pendingStart = 0;
var pending = NOOP;
function flushPending() {
neverFlushed = neverFlushed && (pending === NOOP);
return pending(delegate, pendingStart, pendingLength);
}
function process(op, start, length) {
if (length > 0) {
if (pending !== op) {
if (!flushPending()) {
return false;
}
pending = op;
pendingStart = start;
pendingLength = length;
}
else {
pendingLength += length;
}
}
return true;
}
return {
insert: function (bStart, length) {
return process(INSERT, bStart - bOffset, length);
},
match: function (aStart, length) {
return process(MATCH, aStart - aOffset, length);
},
delete: function (aStart, length) {
// We discard all deletes, but must flush pending matches.
// INSERT,DELETE,INSERT can be coalesced to drop the DELETE;
// MATCH,DELETE,MATCH cannot.
if (pending === INSERT) {
return true;
}
var r = flushPending();
pending = NOOP;
return r;
},
close: function (aLength, bLength) {
if (neverFlushed) {
if (pending === INSERT) {
return exports.REPLACE;
}
else if (pendingStart === 0 && pendingLength === aLength) {
return exports.NO_CHANGE;
}
}
return flushPending() && delegate.close() ? exports.SUCCESS : exports.REPLACE;
}
};
}
exports.coalesce = coalesce;
/**
* Implementation of Myer's diff with the linear space refinement.
*
* Diff the subset of two buffers, as specified by offset/length parameters,
* with differences written to the provided script.
*
* See E. Myers (1986). "An O(ND) Difference Algorithm and Its Variations".
* Algorithmica 1 (2): 251–266.
* http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.4.6927
*
* For more documentation, refer to the Java implementation.
*
* The bail out factor determines when to give up in middlesnake and produce a
* more approximate result. Larger values increase the precision at the cost of
* increased CPU.
*
* The number of diagonals to search in each middleSnake call is bounded by
* max(256, bailOutFactor/100 * totalLength)
*
* For inputs with a lot of small differences, a smaller bailOutFactor often
* has a beneficial effect of moderately reducing the size of the result.
* But it can also increase the size, occasionally dramatically.
* Additionally, the reduced precision affects the quality of JSON
* structural deltas. YMMV, as they say.
*/
var MyersBinaryDiff = /** @class */ (function () {
/**
* Create a MyersBinaryDiff object
*
* @param maximumStorage maximum storage limit
* @param bailOutFactor bail-out limit factor
*/
function MyersBinaryDiff(maximumStorage, bailOutFactor) {
if (maximumStorage === void 0) { maximumStorage = MAXIMUM_STORAGE; }
if (bailOutFactor === void 0) { bailOutFactor = BAIL_OUT_FACTOR; }
this.bailOutFactor = bailOutFactor;
this.storage = new Storage(maximumStorage);
}
/**
* @param a source data
* @param aOffset start of source data
* @param aLength length of source data
* @param b target data
* @param bOffset start of target data
* @param bLength length of target data
* @param editScript the edit script
*/
MyersBinaryDiff.prototype.diff = function (a, aOffset, aLength, b, bOffset, bLength, editScript) {
var script = coalesce(editScript, aOffset, bOffset);
var execution = new Execution(this.storage, a, b, script, this.bailOutFactor);
if (!execution.diff(aOffset, aLength, bOffset, bLength)) {
return exports.REPLACE;
}
return script.close(aLength, bLength);
};
return MyersBinaryDiff;
}());
exports.MyersBinaryDiff = MyersBinaryDiff;