array-changes-async
Version:
281 lines (251 loc) • 11 kB
JavaScript
/*global setTimeout */
var arrayDiff = require('arraydiff-async');
var MAX_STACK_DEPTH = 1000;
function extend(target) {
for (var i = 1; i < arguments.length; i += 1) {
var source = arguments[i];
Object.keys(source).forEach(function (key) {
target[key] = source[key];
});
}
return target;
}
module.exports = function arrayChanges(actual, expected, equal, similar, includeNonNumericalProperties, arrayChangesCallback) {
if (typeof includeNonNumericalProperties === 'function') {
arrayChangesCallback = includeNonNumericalProperties;
includeNonNumericalProperties = false;
}
var mutatedArray = new Array(actual.length);
for (var k = 0; k < actual.length; k += 1) {
mutatedArray[k] = {
type: 'similar',
actualIndex: k,
value: actual[k]
};
}
similar = similar || function (a, b, aIndex, bIndex, callback) {
return callback(false);
};
arrayDiff([].concat(actual), [].concat(expected), function (a, b, aIndex, bIndex, callback) {
equal(a, b, aIndex, bIndex, function (isEqual) {
if (isEqual) {
return callback(true);
}
similar(a, b, aIndex, bIndex, function (isSimilar) {
return callback(isSimilar);
});
});
}, function (itemsDiff) {
function offsetIndex(index) {
var currentOffsetIndex = 0;
var i;
for (i = 0; i < mutatedArray.length && currentOffsetIndex < index; i += 1) {
if (mutatedArray[i].type !== 'remove' && mutatedArray[i].type !== 'moveSource') {
currentOffsetIndex += 1;
}
}
return i;
}
var removes = itemsDiff.filter(function (diffItem) {
return diffItem.type === 'remove';
});
var removedItems = 0;
removes.forEach(function (diffItem) {
var removeIndex = removedItems + diffItem.index;
mutatedArray.slice(removeIndex, diffItem.howMany + removeIndex).forEach(function (v) {
v.type = 'remove';
});
removedItems += diffItem.howMany;
});
var moves = itemsDiff.filter(function (diffItem) {
return diffItem.type === 'move';
});
moves.forEach(function (diffItem) {
var moveFromIndex = offsetIndex(diffItem.from + 1) - 1;
var removed = mutatedArray.slice(moveFromIndex, diffItem.howMany + moveFromIndex);
var added = removed.map(function (v) {
return extend({}, v, { last: false, type: 'moveTarget' });
});
removed.forEach(function (v) {
v.type = 'moveSource';
});
var insertIndex = offsetIndex(diffItem.to);
Array.prototype.splice.apply(mutatedArray, [insertIndex, 0].concat(added));
});
var inserts = itemsDiff.filter(function (diffItem) {
return diffItem.type === 'insert';
});
inserts.forEach(function (diffItem) {
var added = new Array(diffItem.values.length);
for (var i = 0 ; i < diffItem.values.length ; i += 1) {
added[i] = {
type: 'insert',
value: diffItem.values[i]
};
}
Array.prototype.splice.apply(mutatedArray, [offsetIndex(diffItem.index), 0].concat(added));
});
var offset = 0;
mutatedArray.forEach(function (diffItem, index) {
var type = diffItem.type;
if (type === 'remove' || type === 'moveSource') {
offset -= 1;
} else if (type === 'similar') {
diffItem.expected = expected[offset + index];
diffItem.expectedIndex = offset + index;
}
});
var conflicts = mutatedArray.reduce(function (conflicts, item) {
return item.type === 'similar' || item.type === 'moveSource' || item.type === 'moveTarget' ? conflicts : conflicts + 1;
}, 0);
var end = Math.max(actual.length, expected.length);
var countConflicts = function (i, c, stackCallsRemaining, callback) {
if (i >= end || c > conflicts) {
// Do a setTimeout to let the stack unwind
return setTimeout(function () {
callback(c);
}, 0);
}
similar(actual[i], expected[i], i, i, function (areSimilar) {
if (!areSimilar) {
c += 1;
if (stackCallsRemaining === 0) {
return setTimeout(function () {
countConflicts(i + 1, c, MAX_STACK_DEPTH, callback);
});
}
return countConflicts(i + 1, c, stackCallsRemaining - 1, callback);
}
equal(actual[i], expected[i], i, i, function (areEqual) {
if (!areEqual) {
c += 1;
}
if (stackCallsRemaining === 0) {
return setTimeout(function () {
countConflicts(i + 1, c, MAX_STACK_DEPTH, callback);
});
}
return countConflicts(i + 1, c, stackCallsRemaining - 1, callback);
});
});
};
countConflicts(0, 0, MAX_STACK_DEPTH, function (c) {
if (c <= conflicts) {
mutatedArray = [];
var j;
for (j = 0; j < Math.min(actual.length, expected.length); j += 1) {
mutatedArray.push({
type: 'similar',
actualIndex: j,
expectedIndex: j,
value: actual[j],
expected: expected[j]
});
}
if (actual.length < expected.length) {
for (; j < Math.max(actual.length, expected.length); j += 1) {
mutatedArray.push({
type: 'insert',
value: expected[j]
});
}
} else {
for (; j < Math.max(actual.length, expected.length); j += 1) {
mutatedArray.push({
type: 'remove',
value: actual[j]
});
}
}
}
var setEqual = function (i, stackCallsRemaining, callback) {
if (i >= mutatedArray.length) {
return callback();
}
var diffItem = mutatedArray[i];
if (diffItem.type === 'similar') {
return equal(diffItem.value, diffItem.expected, diffItem.actualIndex, diffItem.expectedIndex, function (areEqual) {
if (areEqual) {
mutatedArray[i].type = 'equal';
}
if (stackCallsRemaining === 0) {
return setTimeout(function () {
setEqual(i + 1, MAX_STACK_DEPTH, callback);
});
}
setEqual(i + 1, stackCallsRemaining - 1, callback);
});
}
if (stackCallsRemaining === 0) {
return setTimeout(function () {
setEqual(i + 1, MAX_STACK_DEPTH, callback);
});
}
return setEqual(i + 1, stackCallsRemaining - 1, callback);
};
if (includeNonNumericalProperties) {
var nonNumericalKeys;
if (Array.isArray(includeNonNumericalProperties)) {
nonNumericalKeys = includeNonNumericalProperties;
} else {
var isSeenByNonNumericalKey = {};
nonNumericalKeys = [];
[actual, expected].forEach(function (obj) {
Object.keys(obj).forEach(function (key) {
if (!/^(?:0|[1-9][0-9]*)$/.test(key) && !isSeenByNonNumericalKey[key]) {
isSeenByNonNumericalKey[key] = true;
nonNumericalKeys.push(key);
}
});
if (Object.getOwnPropertySymbols) {
Object.getOwnPropertySymbols(obj).forEach(function (symbol) {
if (!isSeenByNonNumericalKey[symbol]) {
isSeenByNonNumericalKey[symbol] = true;
nonNumericalKeys.push(symbol);
}
});
}
});
}
nonNumericalKeys.forEach(function (key) {
var valueExpected;
if (key in actual) {
var valueActual = actual[key];
valueExpected = expected[key];
if (key in expected && typeof valueExpected !== 'undefined') {
valueExpected = expected[key];
mutatedArray.push({
type: 'similar',
expectedIndex: key,
actualIndex: key,
value: valueActual,
expected: valueExpected
});
} else if (typeof valueActual !== 'undefined') {
mutatedArray.push({
type: 'remove',
actualIndex: key,
value: valueActual
});
}
} else {
valueExpected = expected[key];
if (typeof valueExpected !== 'undefined') {
mutatedArray.push({
type: 'insert',
expectedIndex: key,
value: valueExpected
});
}
}
});
}
setEqual(0, MAX_STACK_DEPTH, function () {
if (mutatedArray.length > 0) {
mutatedArray[mutatedArray.length - 1].last = true;
}
arrayChangesCallback(mutatedArray);
});
});
});
};