UNPKG

diffusion

Version:

Diffusion JavaScript client

553 lines (552 loc) 22 kB
"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;