diffusion
Version:
Diffusion JavaScript client
237 lines (183 loc) • 6.35 kB
JavaScript
/*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;
}
}