From 68fa78293033ea573f0f96f74601c0add8ff500a Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Mon, 27 Apr 2015 15:02:44 -0700 Subject: [PATCH] Separate diff functions from minimongo --- packages/ddp-client/livedata_connection.js | 2 +- packages/ddp-client/package.js | 3 +- packages/diff-sequence/diff.js | 251 +++++++++++++++++++++ packages/diff-sequence/package.js | 22 ++ packages/diff-sequence/tests.js | 160 +++++++++++++ packages/minimongo/diff.js | 216 +----------------- packages/minimongo/minimongo.js | 27 +-- packages/minimongo/minimongo_tests.js | 161 ------------- packages/minimongo/observe.js | 6 +- packages/minimongo/package.js | 4 +- packages/mongo/oplog_observe_driver.js | 2 +- packages/mongo/package.js | 2 +- 12 files changed, 453 insertions(+), 403 deletions(-) create mode 100644 packages/diff-sequence/diff.js create mode 100644 packages/diff-sequence/package.js create mode 100644 packages/diff-sequence/tests.js diff --git a/packages/ddp-client/livedata_connection.js b/packages/ddp-client/livedata_connection.js index c3783fbfd0..6052f116e4 100644 --- a/packages/ddp-client/livedata_connection.js +++ b/packages/ddp-client/livedata_connection.js @@ -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); } diff --git a/packages/ddp-client/package.js b/packages/ddp-client/package.js index fb6b1a80c2..b06623c3dc 100644 --- a/packages/ddp-client/package.js +++ b/packages/ddp-client/package.js @@ -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']); diff --git a/packages/diff-sequence/diff.js b/packages/diff-sequence/diff.js new file mode 100644 index 0000000000..bc5983e57e --- /dev/null +++ b/packages/diff-sequence/diff.js @@ -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 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; + }); +}; + diff --git a/packages/diff-sequence/package.js b/packages/diff-sequence/package.js new file mode 100644 index 0000000000..57f972b1a8 --- /dev/null +++ b/packages/diff-sequence/package.js @@ -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' + ]); +}); + + diff --git a/packages/diff-sequence/tests.js b/packages/diff-sequence/tests.js new file mode 100644 index 0000000000..a8cc9c3c58 --- /dev/null +++ b/packages/diff-sequence/tests.js @@ -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]); +}); + diff --git a/packages/minimongo/diff.js b/packages/minimongo/diff.js index 29c2367cbe..68bfca9c0c 100644 --- a/packages/minimongo/diff.js +++ b/packages/minimongo/diff.js @@ -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 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); }; diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index a62a390ba2..3e1123e2b8 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -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; -}; diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 43dce8dae5..f3deb41c15 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -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(), diff --git a/packages/minimongo/observe.js b/packages/minimongo/observe.js index 191b78333f..bd9f2472e1 100644 --- a/packages/minimongo/observe.js +++ b/packages/minimongo/observe.js @@ -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))); } diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index 9600dc3eb6..2723160918 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -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', diff --git a/packages/mongo/oplog_observe_driver.js b/packages/mongo/oplog_observe_driver.js index 14f77cabeb..7f0da1f533 100644 --- a/packages/mongo/oplog_observe_driver.js +++ b/packages/mongo/oplog_observe_driver.js @@ -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); diff --git a/packages/mongo/package.js b/packages/mongo/package.js index eba7d9bb60..730fff6321 100644 --- a/packages/mongo/package.js +++ b/packages/mongo/package.js @@ -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']);