Separate diff functions from minimongo

This commit is contained in:
Slava Kim
2015-04-27 15:02:44 -07:00
parent 22acddadb1
commit 68fa782930
12 changed files with 453 additions and 403 deletions

View File

@@ -1292,7 +1292,7 @@ _.extend(Connection.prototype, {
if (serverDoc) {
if (serverDoc.document === undefined)
throw new Error("Server sent changed for nonexisting id: " + msg.id);
LocalCollection._applyChanges(serverDoc.document, msg.fields);
DiffSequence.applyChanges(serverDoc.document, msg.fields);
} else {
self._pushUpdate(updates, msg.collection, msg);
}

View File

@@ -18,7 +18,8 @@ Package.onUse(function (api) {
api.use('reload', 'client', {weak: true});
// we depend on LocalCollection._diffObjects, _applyChanges,
// we depend on _diffObjects, _applyChanges,
api.use('diff-sequence', ['client', 'server']);
// _idParse, _idStringify.
api.use('minimongo', ['client', 'server']);

View File

@@ -0,0 +1,251 @@
DiffSequence = {};
// ordered: bool.
// old_results and new_results: collections of documents.
// if ordered, they are arrays.
// if unordered, they are IdMaps
DiffSequence.diffQueryChanges = function (ordered, oldResults, newResults,
observer, options) {
if (ordered)
DiffSequence.diffQueryOrderedChanges(
oldResults, newResults, observer, options);
else
DiffSequence.diffQueryUnorderedChanges(
oldResults, newResults, observer, options);
};
DiffSequence.diffQueryUnorderedChanges = function (oldResults, newResults,
observer, options) {
options = options || {};
var projectionFn = options.projectionFn || EJSON.clone;
if (observer.movedBefore) {
throw new Error("_diffQueryUnordered called with a movedBefore observer!");
}
newResults.forEach(function (newDoc, id) {
var oldDoc = oldResults.get(id);
if (oldDoc) {
if (observer.changed && !EJSON.equals(oldDoc, newDoc)) {
var projectedNew = projectionFn(newDoc);
var projectedOld = projectionFn(oldDoc);
var changedFields =
DiffSequence.makeChangedFields(projectedNew, projectedOld);
if (! _.isEmpty(changedFields)) {
observer.changed(id, changedFields);
}
}
} else if (observer.added) {
var fields = projectionFn(newDoc);
delete fields._id;
observer.added(newDoc._id, fields);
}
});
if (observer.removed) {
oldResults.forEach(function (oldDoc, id) {
if (!newResults.has(id))
observer.removed(id);
});
}
};
DiffSequence.diffQueryOrderedChanges = function (old_results, new_results,
observer, options) {
options = options || {};
var projectionFn = options.projectionFn || EJSON.clone;
var new_presence_of_id = {};
_.each(new_results, function (doc) {
if (new_presence_of_id[doc._id])
Meteor._debug("Duplicate _id in new_results");
new_presence_of_id[doc._id] = true;
});
var old_index_of_id = {};
_.each(old_results, function (doc, i) {
if (doc._id in old_index_of_id)
Meteor._debug("Duplicate _id in old_results");
old_index_of_id[doc._id] = i;
});
// ALGORITHM:
//
// To determine which docs should be considered "moved" (and which
// merely change position because of other docs moving) we run
// a "longest common subsequence" (LCS) algorithm. The LCS of the
// old doc IDs and the new doc IDs gives the docs that should NOT be
// considered moved.
// To actually call the appropriate callbacks to get from the old state to the
// new state:
// First, we call removed() on all the items that only appear in the old
// state.
// Then, once we have the items that should not move, we walk through the new
// results array group-by-group, where a "group" is a set of items that have
// moved, anchored on the end by an item that should not move. One by one, we
// move each of those elements into place "before" the anchoring end-of-group
// item, and fire changed events on them if necessary. Then we fire a changed
// event on the anchor, and move on to the next group. There is always at
// least one group; the last group is anchored by a virtual "null" id at the
// end.
// Asymptotically: O(N k) where k is number of ops, or potentially
// O(N log N) if inner loop of LCS were made to be binary search.
//////// LCS (longest common sequence, with respect to _id)
// (see Wikipedia article on Longest Increasing Subsequence,
// where the LIS is taken of the sequence of old indices of the
// docs in new_results)
//
// unmoved: the output of the algorithm; members of the LCS,
// in the form of indices into new_results
var unmoved = [];
// max_seq_len: length of LCS found so far
var max_seq_len = 0;
// seq_ends[i]: the index into new_results of the last doc in a
// common subsequence of length of i+1 <= max_seq_len
var N = new_results.length;
var seq_ends = new Array(N);
// ptrs: the common subsequence ending with new_results[n] extends
// a common subsequence ending with new_results[ptr[n]], unless
// ptr[n] is -1.
var ptrs = new Array(N);
// virtual sequence of old indices of new results
var old_idx_seq = function(i_new) {
return old_index_of_id[new_results[i_new]._id];
};
// for each item in new_results, use it to extend a common subsequence
// of length j <= max_seq_len
for(var i=0; i<N; i++) {
if (old_index_of_id[new_results[i]._id] !== undefined) {
var j = max_seq_len;
// this inner loop would traditionally be a binary search,
// but scanning backwards we will likely find a subseq to extend
// pretty soon, bounded for example by the total number of ops.
// If this were to be changed to a binary search, we'd still want
// to scan backwards a bit as an optimization.
while (j > 0) {
if (old_idx_seq(seq_ends[j-1]) < old_idx_seq(i))
break;
j--;
}
ptrs[i] = (j === 0 ? -1 : seq_ends[j-1]);
seq_ends[j] = i;
if (j+1 > max_seq_len)
max_seq_len = j+1;
}
}
// pull out the LCS/LIS into unmoved
var idx = (max_seq_len === 0 ? -1 : seq_ends[max_seq_len-1]);
while (idx >= 0) {
unmoved.push(idx);
idx = ptrs[idx];
}
// the unmoved item list is built backwards, so fix that
unmoved.reverse();
// the last group is always anchored by the end of the result list, which is
// an id of "null"
unmoved.push(new_results.length);
_.each(old_results, function (doc) {
if (!new_presence_of_id[doc._id])
observer.removed && observer.removed(doc._id);
});
// for each group of things in the new_results that is anchored by an unmoved
// element, iterate through the things before it.
var startOfGroup = 0;
_.each(unmoved, function (endOfGroup) {
var groupId = new_results[endOfGroup] ? new_results[endOfGroup]._id : null;
var oldDoc, newDoc, fields, projectedNew, projectedOld;
for (var i = startOfGroup; i < endOfGroup; i++) {
newDoc = new_results[i];
if (!_.has(old_index_of_id, newDoc._id)) {
fields = projectionFn(newDoc);
delete fields._id;
observer.addedBefore && observer.addedBefore(newDoc._id, fields, groupId);
observer.added && observer.added(newDoc._id, fields);
} else {
// moved
oldDoc = old_results[old_index_of_id[newDoc._id]];
projectedNew = projectionFn(newDoc);
projectedOld = projectionFn(oldDoc);
fields = DiffSequence.makeChangedFields(projectedNew, projectedOld);
if (!_.isEmpty(fields)) {
observer.changed && observer.changed(newDoc._id, fields);
}
observer.movedBefore && observer.movedBefore(newDoc._id, groupId);
}
}
if (groupId) {
newDoc = new_results[endOfGroup];
oldDoc = old_results[old_index_of_id[newDoc._id]];
projectedNew = projectionFn(newDoc);
projectedOld = projectionFn(oldDoc);
fields = DiffSequence.makeChangedFields(projectedNew, projectedOld);
if (!_.isEmpty(fields)) {
observer.changed && observer.changed(newDoc._id, fields);
}
}
startOfGroup = endOfGroup+1;
});
};
// General helper for diff-ing two objects.
// callbacks is an object like so:
// { leftOnly: function (key, leftValue) {...},
// rightOnly: function (key, rightValue) {...},
// both: function (key, leftValue, rightValue) {...},
// }
DiffSequence.diffObjects = function (left, right, callbacks) {
_.each(left, function (leftValue, key) {
if (_.has(right, key))
callbacks.both && callbacks.both(key, leftValue, right[key]);
else
callbacks.leftOnly && callbacks.leftOnly(key, leftValue);
});
if (callbacks.rightOnly) {
_.each(right, function(rightValue, key) {
if (!_.has(left, key))
callbacks.rightOnly(key, rightValue);
});
}
};
DiffSequence.makeChangedFields = function (newDoc, oldDoc) {
var fields = {};
DiffSequence.diffObjects(oldDoc, newDoc, {
leftOnly: function (key, value) {
fields[key] = undefined;
},
rightOnly: function (key, value) {
fields[key] = value;
},
both: function (key, leftValue, rightValue) {
if (!EJSON.equals(leftValue, rightValue))
fields[key] = rightValue;
}
});
return fields;
};
DiffSequence.applyChanges = function (doc, changeFields) {
_.each(changeFields, function (value, key) {
if (value === undefined)
delete doc[key];
else
doc[key] = value;
});
};

View File

@@ -0,0 +1,22 @@
Package.describe({
summary: "An implementation of a diff algorithm on arrays and objects.",
version: '1.0.0'
});
Package.onUse(function (api) {
api.export('DiffSequence');
api.use(['underscore', 'ejson']);
api.addFiles([
'diff.js'
]);
});
Package.onTest(function (api) {
api.use('tinytest');
api.use('diff-sequence');
api.addFiles([
'tests.js'
]);
});

View File

@@ -0,0 +1,160 @@
Tinytest.add("diff-sequence - diff changes ordering", function (test) {
var makeDocs = function (ids) {
return _.map(ids, function (id) { return {_id: id};});
};
var testMutation = function (a, b) {
var aa = makeDocs(a);
var bb = makeDocs(b);
var aaCopy = EJSON.clone(aa);
DiffSequence.diffQueryOrderedChanges(aa, bb, {
addedBefore: function (id, doc, before) {
if (before === null) {
aaCopy.push( _.extend({_id: id}, doc));
return;
}
for (var i = 0; i < aaCopy.length; i++) {
if (aaCopy[i]._id === before) {
aaCopy.splice(i, 0, _.extend({_id: id}, doc));
return;
}
}
},
movedBefore: function (id, before) {
var found;
for (var i = 0; i < aaCopy.length; i++) {
if (aaCopy[i]._id === id) {
found = aaCopy[i];
aaCopy.splice(i, 1);
}
}
if (before === null) {
aaCopy.push( _.extend({_id: id}, found));
return;
}
for (i = 0; i < aaCopy.length; i++) {
if (aaCopy[i]._id === before) {
aaCopy.splice(i, 0, _.extend({_id: id}, found));
return;
}
}
},
removed: function (id) {
var found;
for (var i = 0; i < aaCopy.length; i++) {
if (aaCopy[i]._id === id) {
found = aaCopy[i];
aaCopy.splice(i, 1);
}
}
}
});
test.equal(aaCopy, bb);
};
var testBothWays = function (a, b) {
testMutation(a, b);
testMutation(b, a);
};
testBothWays(["a", "b", "c"], ["c", "b", "a"]);
testBothWays(["a", "b", "c"], []);
testBothWays(["a", "b", "c"], ["e","f"]);
testBothWays(["a", "b", "c", "d"], ["c", "b", "a"]);
testBothWays(['A','B','C','D','E','F','G','H','I'],
['A','B','F','G','C','D','I','L','M','N','H']);
testBothWays(['A','B','C','D','E','F','G','H','I'],['A','B','C','D','F','G','H','E','I']);
});
Tinytest.add("diff-sequence - diff", function (test) {
// test correctness
var diffTest = function(origLen, newOldIdx) {
var oldResults = new Array(origLen);
for (var i = 1; i <= origLen; i++)
oldResults[i-1] = {_id: i};
var newResults = _.map(newOldIdx, function(n) {
var doc = {_id: Math.abs(n)};
if (n < 0)
doc.changed = true;
return doc;
});
var find = function (arr, id) {
for (var i = 0; i < arr.length; i++) {
if (EJSON.equals(arr[i]._id, id))
return i;
}
return -1;
};
var results = _.clone(oldResults);
var observer = {
addedBefore: function(id, fields, before) {
var before_idx;
if (before === null)
before_idx = results.length;
else
before_idx = find (results, before);
var doc = _.extend({_id: id}, fields);
test.isFalse(before_idx < 0 || before_idx > results.length);
results.splice(before_idx, 0, doc);
},
removed: function(id) {
var at_idx = find (results, id);
test.isFalse(at_idx < 0 || at_idx >= results.length);
results.splice(at_idx, 1);
},
changed: function(id, fields) {
var at_idx = find (results, id);
var oldDoc = results[at_idx];
var doc = EJSON.clone(oldDoc);
DiffSequence.applyChanges(doc, fields);
test.isFalse(at_idx < 0 || at_idx >= results.length);
test.equal(doc._id, oldDoc._id);
results[at_idx] = doc;
},
movedBefore: function(id, before) {
var old_idx = find(results, id);
var new_idx;
if (before === null)
new_idx = results.length;
else
new_idx = find (results, before);
if (new_idx > old_idx)
new_idx--;
test.isFalse(old_idx < 0 || old_idx >= results.length);
test.isFalse(new_idx < 0 || new_idx >= results.length);
results.splice(new_idx, 0, results.splice(old_idx, 1)[0]);
}
};
DiffSequence.diffQueryOrderedChanges(oldResults, newResults, observer);
test.equal(results, newResults);
};
// edge cases and cases run into during debugging
diffTest(5, [5, 1, 2, 3, 4]);
diffTest(0, [1, 2, 3, 4]);
diffTest(4, []);
diffTest(7, [4, 5, 6, 7, 1, 2, 3]);
diffTest(7, [5, 6, 7, 1, 2, 3, 4]);
diffTest(10, [7, 4, 11, 6, 12, 1, 5]);
diffTest(3, [3, 2, 1]);
diffTest(10, [2, 7, 4, 6, 11, 3, 8, 9]);
diffTest(0, []);
diffTest(1, []);
diffTest(0, [1]);
diffTest(1, [1]);
diffTest(5, [1, 2, 3, 4, 5]);
// interaction between "changed" and other ops
diffTest(5, [-5, -1, 2, -3, 4]);
diffTest(7, [-4, -5, 6, 7, -1, 2, 3]);
diffTest(7, [5, 6, -7, 1, 2, -3, 4]);
diffTest(10, [7, -4, 11, 6, 12, -1, 5]);
diffTest(3, [-3, -2, -1]);
diffTest(10, [-2, 7, 4, 6, 11, -3, -8, 9]);
});

View File

@@ -2,220 +2,20 @@
// old_results and new_results: collections of documents.
// if ordered, they are arrays.
// if unordered, they are IdMaps
LocalCollection._diffQueryChanges = function (ordered, oldResults, newResults,
observer, options) {
if (ordered)
LocalCollection._diffQueryOrderedChanges(
oldResults, newResults, observer, options);
else
LocalCollection._diffQueryUnorderedChanges(
oldResults, newResults, observer, options);
LocalCollection._diffQueryChanges = function (ordered, oldResults, newResults, observer, options) {
return DiffSequence.diffQueryChanges(ordered, oldResults, newResults, observer, options);
};
LocalCollection._diffQueryUnorderedChanges = function (oldResults, newResults,
observer, options) {
options = options || {};
var projectionFn = options.projectionFn || EJSON.clone;
if (observer.movedBefore) {
throw new Error("_diffQueryUnordered called with a movedBefore observer!");
}
newResults.forEach(function (newDoc, id) {
var oldDoc = oldResults.get(id);
if (oldDoc) {
if (observer.changed && !EJSON.equals(oldDoc, newDoc)) {
var projectedNew = projectionFn(newDoc);
var projectedOld = projectionFn(oldDoc);
var changedFields =
LocalCollection._makeChangedFields(projectedNew, projectedOld);
if (! _.isEmpty(changedFields)) {
observer.changed(id, changedFields);
}
}
} else if (observer.added) {
var fields = projectionFn(newDoc);
delete fields._id;
observer.added(newDoc._id, fields);
}
});
if (observer.removed) {
oldResults.forEach(function (oldDoc, id) {
if (!newResults.has(id))
observer.removed(id);
});
}
LocalCollection._diffQueryUnorderedChanges = function (oldResults, newResults, observer, options) {
return DiffSequence.diffQueryUnorderedChanges(oldResults, newResults, observer, options);
};
LocalCollection._diffQueryOrderedChanges = function (old_results, new_results,
observer, options) {
options = options || {};
var projectionFn = options.projectionFn || EJSON.clone;
var new_presence_of_id = {};
_.each(new_results, function (doc) {
if (new_presence_of_id[doc._id])
Meteor._debug("Duplicate _id in new_results");
new_presence_of_id[doc._id] = true;
});
var old_index_of_id = {};
_.each(old_results, function (doc, i) {
if (doc._id in old_index_of_id)
Meteor._debug("Duplicate _id in old_results");
old_index_of_id[doc._id] = i;
});
// ALGORITHM:
//
// To determine which docs should be considered "moved" (and which
// merely change position because of other docs moving) we run
// a "longest common subsequence" (LCS) algorithm. The LCS of the
// old doc IDs and the new doc IDs gives the docs that should NOT be
// considered moved.
// To actually call the appropriate callbacks to get from the old state to the
// new state:
// First, we call removed() on all the items that only appear in the old
// state.
// Then, once we have the items that should not move, we walk through the new
// results array group-by-group, where a "group" is a set of items that have
// moved, anchored on the end by an item that should not move. One by one, we
// move each of those elements into place "before" the anchoring end-of-group
// item, and fire changed events on them if necessary. Then we fire a changed
// event on the anchor, and move on to the next group. There is always at
// least one group; the last group is anchored by a virtual "null" id at the
// end.
// Asymptotically: O(N k) where k is number of ops, or potentially
// O(N log N) if inner loop of LCS were made to be binary search.
//////// LCS (longest common sequence, with respect to _id)
// (see Wikipedia article on Longest Increasing Subsequence,
// where the LIS is taken of the sequence of old indices of the
// docs in new_results)
//
// unmoved: the output of the algorithm; members of the LCS,
// in the form of indices into new_results
var unmoved = [];
// max_seq_len: length of LCS found so far
var max_seq_len = 0;
// seq_ends[i]: the index into new_results of the last doc in a
// common subsequence of length of i+1 <= max_seq_len
var N = new_results.length;
var seq_ends = new Array(N);
// ptrs: the common subsequence ending with new_results[n] extends
// a common subsequence ending with new_results[ptr[n]], unless
// ptr[n] is -1.
var ptrs = new Array(N);
// virtual sequence of old indices of new results
var old_idx_seq = function(i_new) {
return old_index_of_id[new_results[i_new]._id];
};
// for each item in new_results, use it to extend a common subsequence
// of length j <= max_seq_len
for(var i=0; i<N; i++) {
if (old_index_of_id[new_results[i]._id] !== undefined) {
var j = max_seq_len;
// this inner loop would traditionally be a binary search,
// but scanning backwards we will likely find a subseq to extend
// pretty soon, bounded for example by the total number of ops.
// If this were to be changed to a binary search, we'd still want
// to scan backwards a bit as an optimization.
while (j > 0) {
if (old_idx_seq(seq_ends[j-1]) < old_idx_seq(i))
break;
j--;
}
ptrs[i] = (j === 0 ? -1 : seq_ends[j-1]);
seq_ends[j] = i;
if (j+1 > max_seq_len)
max_seq_len = j+1;
}
}
// pull out the LCS/LIS into unmoved
var idx = (max_seq_len === 0 ? -1 : seq_ends[max_seq_len-1]);
while (idx >= 0) {
unmoved.push(idx);
idx = ptrs[idx];
}
// the unmoved item list is built backwards, so fix that
unmoved.reverse();
// the last group is always anchored by the end of the result list, which is
// an id of "null"
unmoved.push(new_results.length);
_.each(old_results, function (doc) {
if (!new_presence_of_id[doc._id])
observer.removed && observer.removed(doc._id);
});
// for each group of things in the new_results that is anchored by an unmoved
// element, iterate through the things before it.
var startOfGroup = 0;
_.each(unmoved, function (endOfGroup) {
var groupId = new_results[endOfGroup] ? new_results[endOfGroup]._id : null;
var oldDoc, newDoc, fields, projectedNew, projectedOld;
for (var i = startOfGroup; i < endOfGroup; i++) {
newDoc = new_results[i];
if (!_.has(old_index_of_id, newDoc._id)) {
fields = projectionFn(newDoc);
delete fields._id;
observer.addedBefore && observer.addedBefore(newDoc._id, fields, groupId);
observer.added && observer.added(newDoc._id, fields);
} else {
// moved
oldDoc = old_results[old_index_of_id[newDoc._id]];
projectedNew = projectionFn(newDoc);
projectedOld = projectionFn(oldDoc);
fields = LocalCollection._makeChangedFields(projectedNew, projectedOld);
if (!_.isEmpty(fields)) {
observer.changed && observer.changed(newDoc._id, fields);
}
observer.movedBefore && observer.movedBefore(newDoc._id, groupId);
}
}
if (groupId) {
newDoc = new_results[endOfGroup];
oldDoc = old_results[old_index_of_id[newDoc._id]];
projectedNew = projectionFn(newDoc);
projectedOld = projectionFn(oldDoc);
fields = LocalCollection._makeChangedFields(projectedNew, projectedOld);
if (!_.isEmpty(fields)) {
observer.changed && observer.changed(newDoc._id, fields);
}
}
startOfGroup = endOfGroup+1;
});
LocalCollection._diffQueryOrderedChanges =
function (oldResults, newResults, observer, options) {
return DiffSequence.diffQueryOrderedChanges(oldResults, newResults, observer, options);
};
// General helper for diff-ing two objects.
// callbacks is an object like so:
// { leftOnly: function (key, leftValue) {...},
// rightOnly: function (key, rightValue) {...},
// both: function (key, leftValue, rightValue) {...},
// }
LocalCollection._diffObjects = function (left, right, callbacks) {
_.each(left, function (leftValue, key) {
if (_.has(right, key))
callbacks.both && callbacks.both(key, leftValue, right[key]);
else
callbacks.leftOnly && callbacks.leftOnly(key, leftValue);
});
if (callbacks.rightOnly) {
_.each(right, function(rightValue, key) {
if (!_.has(left, key))
callbacks.rightOnly(key, rightValue);
});
}
return DiffSequence.diffObjects(left, right, callbacks);
};

View File

@@ -40,15 +40,6 @@ Minimongo = {};
// Use it to export private functions to test in Tinytest.
MinimongoTest = {};
LocalCollection._applyChanges = function (doc, changeFields) {
_.each(changeFields, function (value, key) {
if (value === undefined)
delete doc[key];
else
doc[key] = value;
});
};
MinimongoError = function (message) {
var e = new Error(message);
e.name = "MinimongoError";
@@ -873,7 +864,7 @@ LocalCollection._updateInResults = function (query, doc, old_doc) {
if (!EJSON.equals(doc._id, old_doc._id))
throw new Error("Can't change a doc's _id while updating");
var projectionFn = query.projectionFn;
var changedFields = LocalCollection._makeChangedFields(
var changedFields = DiffSequence.makeChangedFields(
projectionFn(doc), projectionFn(old_doc));
if (!query.ordered) {
@@ -1094,19 +1085,3 @@ LocalCollection._idParse = function (id) {
}
};
LocalCollection._makeChangedFields = function (newDoc, oldDoc) {
var fields = {};
LocalCollection._diffObjects(oldDoc, newDoc, {
leftOnly: function (key, value) {
fields[key] = undefined;
},
rightOnly: function (key, value) {
fields[key] = value;
},
both: function (key, leftValue, rightValue) {
if (!EJSON.equals(leftValue, rightValue))
fields[key] = rightValue;
}
});
return fields;
};

View File

@@ -2521,167 +2521,6 @@ _.each([true, false], function (ordered) {
});
Tinytest.add("minimongo - diff changes ordering", function (test) {
var makeDocs = function (ids) {
return _.map(ids, function (id) { return {_id: id};});
};
var testMutation = function (a, b) {
var aa = makeDocs(a);
var bb = makeDocs(b);
var aaCopy = EJSON.clone(aa);
LocalCollection._diffQueryOrderedChanges(aa, bb, {
addedBefore: function (id, doc, before) {
if (before === null) {
aaCopy.push( _.extend({_id: id}, doc));
return;
}
for (var i = 0; i < aaCopy.length; i++) {
if (aaCopy[i]._id === before) {
aaCopy.splice(i, 0, _.extend({_id: id}, doc));
return;
}
}
},
movedBefore: function (id, before) {
var found;
for (var i = 0; i < aaCopy.length; i++) {
if (aaCopy[i]._id === id) {
found = aaCopy[i];
aaCopy.splice(i, 1);
}
}
if (before === null) {
aaCopy.push( _.extend({_id: id}, found));
return;
}
for (i = 0; i < aaCopy.length; i++) {
if (aaCopy[i]._id === before) {
aaCopy.splice(i, 0, _.extend({_id: id}, found));
return;
}
}
},
removed: function (id) {
var found;
for (var i = 0; i < aaCopy.length; i++) {
if (aaCopy[i]._id === id) {
found = aaCopy[i];
aaCopy.splice(i, 1);
}
}
}
});
test.equal(aaCopy, bb);
};
var testBothWays = function (a, b) {
testMutation(a, b);
testMutation(b, a);
};
testBothWays(["a", "b", "c"], ["c", "b", "a"]);
testBothWays(["a", "b", "c"], []);
testBothWays(["a", "b", "c"], ["e","f"]);
testBothWays(["a", "b", "c", "d"], ["c", "b", "a"]);
testBothWays(['A','B','C','D','E','F','G','H','I'],
['A','B','F','G','C','D','I','L','M','N','H']);
testBothWays(['A','B','C','D','E','F','G','H','I'],['A','B','C','D','F','G','H','E','I']);
});
Tinytest.add("minimongo - diff", function (test) {
// test correctness
var diffTest = function(origLen, newOldIdx) {
var oldResults = new Array(origLen);
for (var i = 1; i <= origLen; i++)
oldResults[i-1] = {_id: i};
var newResults = _.map(newOldIdx, function(n) {
var doc = {_id: Math.abs(n)};
if (n < 0)
doc.changed = true;
return doc;
});
var find = function (arr, id) {
for (var i = 0; i < arr.length; i++) {
if (EJSON.equals(arr[i]._id, id))
return i;
}
return -1;
};
var results = _.clone(oldResults);
var observer = {
addedBefore: function(id, fields, before) {
var before_idx;
if (before === null)
before_idx = results.length;
else
before_idx = find (results, before);
var doc = _.extend({_id: id}, fields);
test.isFalse(before_idx < 0 || before_idx > results.length);
results.splice(before_idx, 0, doc);
},
removed: function(id) {
var at_idx = find (results, id);
test.isFalse(at_idx < 0 || at_idx >= results.length);
results.splice(at_idx, 1);
},
changed: function(id, fields) {
var at_idx = find (results, id);
var oldDoc = results[at_idx];
var doc = EJSON.clone(oldDoc);
LocalCollection._applyChanges(doc, fields);
test.isFalse(at_idx < 0 || at_idx >= results.length);
test.equal(doc._id, oldDoc._id);
results[at_idx] = doc;
},
movedBefore: function(id, before) {
var old_idx = find(results, id);
var new_idx;
if (before === null)
new_idx = results.length;
else
new_idx = find (results, before);
if (new_idx > old_idx)
new_idx--;
test.isFalse(old_idx < 0 || old_idx >= results.length);
test.isFalse(new_idx < 0 || new_idx >= results.length);
results.splice(new_idx, 0, results.splice(old_idx, 1)[0]);
}
};
LocalCollection._diffQueryOrderedChanges(oldResults, newResults, observer);
test.equal(results, newResults);
};
// edge cases and cases run into during debugging
diffTest(5, [5, 1, 2, 3, 4]);
diffTest(0, [1, 2, 3, 4]);
diffTest(4, []);
diffTest(7, [4, 5, 6, 7, 1, 2, 3]);
diffTest(7, [5, 6, 7, 1, 2, 3, 4]);
diffTest(10, [7, 4, 11, 6, 12, 1, 5]);
diffTest(3, [3, 2, 1]);
diffTest(10, [2, 7, 4, 6, 11, 3, 8, 9]);
diffTest(0, []);
diffTest(1, []);
diffTest(0, [1]);
diffTest(1, [1]);
diffTest(5, [1, 2, 3, 4, 5]);
// interaction between "changed" and other ops
diffTest(5, [-5, -1, 2, -3, 4]);
diffTest(7, [-4, -5, 6, 7, -1, 2, 3]);
diffTest(7, [5, 6, -7, 1, 2, -3, 4]);
diffTest(10, [7, -4, 11, 6, 12, -1, 5]);
diffTest(3, [-3, -2, -1]);
diffTest(10, [-2, 7, 4, 6, 11, -3, -8, 9]);
});
Tinytest.add("minimongo - saveOriginals", function (test) {
// set up some data
var c = new LocalCollection(),

View File

@@ -65,7 +65,7 @@ LocalCollection._CachingChangeObserver = function (options) {
throw new Error("Unknown id for changed: " + id);
callbacks.changed && callbacks.changed.call(
self, id, EJSON.clone(fields));
LocalCollection._applyChanges(doc, fields);
DiffSequence.applyChanges(doc, fields);
};
self.applyChange.removed = function (id) {
callbacks.removed && callbacks.removed.call(self, id);
@@ -106,7 +106,7 @@ LocalCollection._observeFromObserveChanges = function (cursor, observeCallbacks)
if (!doc)
throw new Error("Unknown id for changed: " + id);
var oldDoc = transform(EJSON.clone(doc));
LocalCollection._applyChanges(doc, fields);
DiffSequence.applyChanges(doc, fields);
doc = transform(doc);
if (observeCallbacks.changedAt) {
var index = indices ? self.docs.indexOf(id) : -1;
@@ -158,7 +158,7 @@ LocalCollection._observeFromObserveChanges = function (cursor, observeCallbacks)
if (observeCallbacks.changed) {
var oldDoc = self.docs.get(id);
var doc = EJSON.clone(oldDoc);
LocalCollection._applyChanges(doc, fields);
DiffSequence.applyChanges(doc, fields);
observeCallbacks.changed(transform(doc),
transform(EJSON.clone(oldDoc)));
}

View File

@@ -8,9 +8,11 @@ Package.onUse(function (api) {
api.export('Minimongo');
api.export('MinimongoTest', { testOnly: true });
api.use(['underscore', 'json', 'ejson', 'id-map', 'ordered-dict', 'tracker',
'random', 'ordered-dict']);
'random']);
// This package is used for geo-location queries such as $near
api.use('geojson-utils');
// This package is used to get diff results on arrays and objects
api.use('diff-sequence');
api.addFiles([
'minimongo.js',
'wrap_transform.js',

View File

@@ -264,7 +264,7 @@ _.extend(OplogObserveDriver.prototype, {
self._published.set(id, self._sharedProjectionFn(newDoc));
var projectedNew = self._projectionFn(newDoc);
var projectedOld = self._projectionFn(oldDoc);
var changed = LocalCollection._makeChangedFields(
var changed = DiffSequence.makeChangedFields(
projectedNew, projectedOld);
if (!_.isEmpty(changed))
self._multiplexer.changed(id, changed);

View File

@@ -24,7 +24,7 @@ Package.onUse(function (api) {
api.use('npm-mongo', 'server');
api.use(['random', 'ejson', 'json', 'underscore', 'minimongo', 'logging',
'ddp', 'tracker'],
'ddp', 'tracker', 'diff-sequence'],
['client', 'server']);
api.use('check', ['client', 'server']);