UNPKG

diffusion

Version:

Diffusion JavaScript client

237 lines (183 loc) 6.35 kB
/*eslint valid-jsdoc: "off"*/ var _implements = require('util/interface')._implements; var DeltaType = require('../../../data/delta-type'); var BufferOutputStream = require('io/buffer-output-stream'); var BinaryDeltaImpl = require('data/binary/binary-delta-impl'); // Meyers diff engine var MyersBinaryDiff = require('data/diff/myers-binary-diff'); // Cbor var Decoder = require('cbor/decoder'); var Encoder = require('cbor/encoder'); // Global CBOR encoder var encoder = new Encoder(); //Global no-change constant var NO_CHANGE = new BinaryDeltaImpl(new Buffer([-10]), 0, 1); module.exports = _implements(DeltaType, function BinaryDeltaSupportImpl(implementation, vToC, cToV) { var binaryDiff = new MyersBinaryDiff(); var self = this; this.name = function() { return "binary"; }; this.diff = function(oldValue, newValue) { oldValue = vToC(oldValue); newValue = vToC(newValue); var buffer = newValue.$buffer; var offset = newValue.$offset; var length = newValue.$length; var budget = length; var script = new Script(encoder, buffer, offset, function(cost) { return (budget -= cost) <= 0; }); // Perform diff var result = binaryDiff.diff( oldValue.$buffer, oldValue.$offset, oldValue.$length, buffer, offset, length, script); // Flush will reset global encoder var delta = encoder.flush(); switch (result) { case MyersBinaryDiff.REPLACE : return replace(newValue); case MyersBinaryDiff.NO_CHANGE : return NO_CHANGE; default : return self.readDelta(delta); } }; this.apply = function(oldValue, delta) { if (!delta || !delta.hasChanges()) { return oldValue; } oldValue = vToC(oldValue); var decoder = new Decoder(delta.$buffer); var bos = new BufferOutputStream(); while (decoder.hasRemaining()) { var start = decoder.nextValue(); if (Buffer.isBuffer(start)) { bos.writeMany(start); } else if (typeof start === "number") { bos.writeMany(oldValue.$buffer, oldValue.$offset + start, decoder.nextValue()); } } return cToV(new implementation(bos.getBuffer())); }; this.readDelta = function(buffer, offset, length) { var delta = new BinaryDeltaImpl(buffer, offset, length); // Because JS handles object equality solely by reference, check (unique) properties if (delta.$length === 1 && buffer[delta.$offset] === NO_CHANGE.$buffer[0]) { return NO_CHANGE; } return delta; }; this.writeDelta = function(delta) { return delta.$buffer.slice(delta.$offset, delta.$offset + delta.$length); }; this.noChange = function() { return NO_CHANGE; }; this.isValueCheaper = function(value, delta) { return value.$length <= delta.$length; }; }); module.exports.NO_CHANGE = NO_CHANGE; function replace(value) { encoder.encode(value.$buffer, value.$offset, value.$length); return new BinaryDeltaImpl(encoder.flush()); } /** * The CBOR cost of an unsigned integer in bytes. It's cheaper to * calculate than flush the encoder and check the number of written * bytes. * * @param {Number} i - The integer to check * @returns {Number} Byte cost */ function cborCost(i) { if (i < 24) { return 1; } else if (i < 0xFF) { return 2; } else if (i <= 0xFFFF) { return 3; } return 5; } /** * @return true if it is worth conflating a match with an adjacent insert */ function conflatableMatch(matchStart, matchLength, insertLength) { var matchCost = cborCost(matchStart) + 1; var insertCost = cborCost(matchLength + insertLength) - cborCost(insertLength) + matchLength; return insertCost <= matchCost; } function Script(encoder, buffer, offset, blowsBudget) { // If there is no pending operation, pendingStart will be -1. // Otherwise pendingStart and pendingEnd will be >=0, and // pendingInsert will be true for an insert or false for a match. var pendingInsert; var pendingLength; var pendingStart = -1; this.insert = function(bStart, length) { if (!pendingInsert && pendingStart !== -1 && conflatableMatch(pendingStart, pendingLength, length)) { pendingStart = bStart - pendingLength; pendingLength += length; pendingInsert = true; return MyersBinaryDiff.SUCCESS; } return process(true, bStart, length); }; this.match = function(aStart, length) { if (pendingInsert && pendingStart !== -1 && conflatableMatch(aStart, length, pendingLength)) { pendingLength += length; return MyersBinaryDiff.SUCCESS; } return process(false, aStart, length); }; this.close = function() { return flush(); }; function flush() { if (pendingStart === -1) { return MyersBinaryDiff.SUCCESS; } else if (pendingInsert) { return writeInsert(pendingStart, pendingLength); } else { return writeMatch(pendingStart, pendingLength); } } function process(insert, start, length) { var r = flush(); if (r !== MyersBinaryDiff.SUCCESS) { return r; } pendingInsert = insert; pendingStart = start; pendingLength = length; return MyersBinaryDiff.SUCCESS; } function writeInsert(start, length) { if (blowsBudget(length + cborCost(length))) { return MyersBinaryDiff.REPLACE; } encoder.encode(buffer, offset + start, length); pendingStart = -1; return MyersBinaryDiff.SUCCESS; } function writeMatch(start, length) { if (blowsBudget(cborCost(start) + cborCost(length))) { return MyersBinaryDiff.REPLACE; } encoder.encode(start); encoder.encode(length); pendingStart = -1; return MyersBinaryDiff.SUCCESS; } }