(function () { /* Imports */ var Meteor = Package.meteor.Meteor; var _ = Package.underscore._; var EJSON = Package.ejson.EJSON; /* Package-scope variables */ var DiffSequence; (function(){ ///////////////////////////////////////////////////////////////////////////////////// // // // packages/diff-sequence/diff.js // // // ///////////////////////////////////////////////////////////////////////////////////// // DiffSequence = {}; // 1 // 2 // ordered: bool. // 3 // old_results and new_results: collections of documents. // 4 // if ordered, they are arrays. // 5 // if unordered, they are IdMaps // 6 DiffSequence.diffQueryChanges = function (ordered, oldResults, newResults, // 7 observer, options) { // 8 if (ordered) // 9 DiffSequence.diffQueryOrderedChanges( // 10 oldResults, newResults, observer, options); // 11 else // 12 DiffSequence.diffQueryUnorderedChanges( // 13 oldResults, newResults, observer, options); // 14 }; // 15 // 16 DiffSequence.diffQueryUnorderedChanges = function (oldResults, newResults, // 17 observer, options) { // 18 options = options || {}; // 19 var projectionFn = options.projectionFn || EJSON.clone; // 20 // 21 if (observer.movedBefore) { // 22 throw new Error("_diffQueryUnordered called with a movedBefore observer!"); // 23 } // 24 // 25 newResults.forEach(function (newDoc, id) { // 26 var oldDoc = oldResults.get(id); // 27 if (oldDoc) { // 28 if (observer.changed && !EJSON.equals(oldDoc, newDoc)) { // 29 var projectedNew = projectionFn(newDoc); // 30 var projectedOld = projectionFn(oldDoc); // 31 var changedFields = // 32 DiffSequence.makeChangedFields(projectedNew, projectedOld); // 33 if (! _.isEmpty(changedFields)) { // 34 observer.changed(id, changedFields); // 35 } // 36 } // 37 } else if (observer.added) { // 38 var fields = projectionFn(newDoc); // 39 delete fields._id; // 40 observer.added(newDoc._id, fields); // 41 } // 42 }); // 43 // 44 if (observer.removed) { // 45 oldResults.forEach(function (oldDoc, id) { // 46 if (!newResults.has(id)) // 47 observer.removed(id); // 48 }); // 49 } // 50 }; // 51 // 52 // 53 DiffSequence.diffQueryOrderedChanges = function (old_results, new_results, // 54 observer, options) { // 55 options = options || {}; // 56 var projectionFn = options.projectionFn || EJSON.clone; // 57 // 58 var new_presence_of_id = {}; // 59 _.each(new_results, function (doc) { // 60 if (new_presence_of_id[doc._id]) // 61 Meteor._debug("Duplicate _id in new_results"); // 62 new_presence_of_id[doc._id] = true; // 63 }); // 64 // 65 var old_index_of_id = {}; // 66 _.each(old_results, function (doc, i) { // 67 if (doc._id in old_index_of_id) // 68 Meteor._debug("Duplicate _id in old_results"); // 69 old_index_of_id[doc._id] = i; // 70 }); // 71 // 72 // ALGORITHM: // 73 // // 74 // To determine which docs should be considered "moved" (and which // 75 // merely change position because of other docs moving) we run // 76 // a "longest common subsequence" (LCS) algorithm. The LCS of the // 77 // old doc IDs and the new doc IDs gives the docs that should NOT be // 78 // considered moved. // 79 // 80 // To actually call the appropriate callbacks to get from the old state to the // 81 // new state: // 82 // 83 // First, we call removed() on all the items that only appear in the old // 84 // state. // 85 // 86 // Then, once we have the items that should not move, we walk through the new // 87 // results array group-by-group, where a "group" is a set of items that have // 88 // moved, anchored on the end by an item that should not move. One by one, we // 89 // move each of those elements into place "before" the anchoring end-of-group // 90 // item, and fire changed events on them if necessary. Then we fire a changed // 91 // event on the anchor, and move on to the next group. There is always at // 92 // least one group; the last group is anchored by a virtual "null" id at the // 93 // end. // 94 // 95 // Asymptotically: O(N k) where k is number of ops, or potentially // 96 // O(N log N) if inner loop of LCS were made to be binary search. // 97 // 98 // 99 //////// LCS (longest common sequence, with respect to _id) // 100 // (see Wikipedia article on Longest Increasing Subsequence, // 101 // where the LIS is taken of the sequence of old indices of the // 102 // docs in new_results) // 103 // // 104 // unmoved: the output of the algorithm; members of the LCS, // 105 // in the form of indices into new_results // 106 var unmoved = []; // 107 // max_seq_len: length of LCS found so far // 108 var max_seq_len = 0; // 109 // seq_ends[i]: the index into new_results of the last doc in a // 110 // common subsequence of length of i+1 <= max_seq_len // 111 var N = new_results.length; // 112 var seq_ends = new Array(N); // 113 // ptrs: the common subsequence ending with new_results[n] extends // 114 // a common subsequence ending with new_results[ptr[n]], unless // 115 // ptr[n] is -1. // 116 var ptrs = new Array(N); // 117 // virtual sequence of old indices of new results // 118 var old_idx_seq = function(i_new) { // 119 return old_index_of_id[new_results[i_new]._id]; // 120 }; // 121 // for each item in new_results, use it to extend a common subsequence // 122 // of length j <= max_seq_len // 123 for(var i=0; i 0) { // 132 if (old_idx_seq(seq_ends[j-1]) < old_idx_seq(i)) // 133 break; // 134 j--; // 135 } // 136 // 137 ptrs[i] = (j === 0 ? -1 : seq_ends[j-1]); // 138 seq_ends[j] = i; // 139 if (j+1 > max_seq_len) // 140 max_seq_len = j+1; // 141 } // 142 } // 143 // 144 // pull out the LCS/LIS into unmoved // 145 var idx = (max_seq_len === 0 ? -1 : seq_ends[max_seq_len-1]); // 146 while (idx >= 0) { // 147 unmoved.push(idx); // 148 idx = ptrs[idx]; // 149 } // 150 // the unmoved item list is built backwards, so fix that // 151 unmoved.reverse(); // 152 // 153 // the last group is always anchored by the end of the result list, which is // 154 // an id of "null" // 155 unmoved.push(new_results.length); // 156 // 157 _.each(old_results, function (doc) { // 158 if (!new_presence_of_id[doc._id]) // 159 observer.removed && observer.removed(doc._id); // 160 }); // 161 // for each group of things in the new_results that is anchored by an unmoved // 162 // element, iterate through the things before it. // 163 var startOfGroup = 0; // 164 _.each(unmoved, function (endOfGroup) { // 165 var groupId = new_results[endOfGroup] ? new_results[endOfGroup]._id : null; // 166 var oldDoc, newDoc, fields, projectedNew, projectedOld; // 167 for (var i = startOfGroup; i < endOfGroup; i++) { // 168 newDoc = new_results[i]; // 169 if (!_.has(old_index_of_id, newDoc._id)) { // 170 fields = projectionFn(newDoc); // 171 delete fields._id; // 172 observer.addedBefore && observer.addedBefore(newDoc._id, fields, groupId); observer.added && observer.added(newDoc._id, fields); // 174 } else { // 175 // moved // 176 oldDoc = old_results[old_index_of_id[newDoc._id]]; // 177 projectedNew = projectionFn(newDoc); // 178 projectedOld = projectionFn(oldDoc); // 179 fields = DiffSequence.makeChangedFields(projectedNew, projectedOld); // 180 if (!_.isEmpty(fields)) { // 181 observer.changed && observer.changed(newDoc._id, fields); // 182 } // 183 observer.movedBefore && observer.movedBefore(newDoc._id, groupId); // 184 } // 185 } // 186 if (groupId) { // 187 newDoc = new_results[endOfGroup]; // 188 oldDoc = old_results[old_index_of_id[newDoc._id]]; // 189 projectedNew = projectionFn(newDoc); // 190 projectedOld = projectionFn(oldDoc); // 191 fields = DiffSequence.makeChangedFields(projectedNew, projectedOld); // 192 if (!_.isEmpty(fields)) { // 193 observer.changed && observer.changed(newDoc._id, fields); // 194 } // 195 } // 196 startOfGroup = endOfGroup+1; // 197 }); // 198 // 199 // 200 }; // 201 // 202 // 203 // General helper for diff-ing two objects. // 204 // callbacks is an object like so: // 205 // { leftOnly: function (key, leftValue) {...}, // 206 // rightOnly: function (key, rightValue) {...}, // 207 // both: function (key, leftValue, rightValue) {...}, // 208 // } // 209 DiffSequence.diffObjects = function (left, right, callbacks) { // 210 _.each(left, function (leftValue, key) { // 211 if (_.has(right, key)) // 212 callbacks.both && callbacks.both(key, leftValue, right[key]); // 213 else // 214 callbacks.leftOnly && callbacks.leftOnly(key, leftValue); // 215 }); // 216 if (callbacks.rightOnly) { // 217 _.each(right, function(rightValue, key) { // 218 if (!_.has(left, key)) // 219 callbacks.rightOnly(key, rightValue); // 220 }); // 221 } // 222 }; // 223 // 224 // 225 DiffSequence.makeChangedFields = function (newDoc, oldDoc) { // 226 var fields = {}; // 227 DiffSequence.diffObjects(oldDoc, newDoc, { // 228 leftOnly: function (key, value) { // 229 fields[key] = undefined; // 230 }, // 231 rightOnly: function (key, value) { // 232 fields[key] = value; // 233 }, // 234 both: function (key, leftValue, rightValue) { // 235 if (!EJSON.equals(leftValue, rightValue)) // 236 fields[key] = rightValue; // 237 } // 238 }); // 239 return fields; // 240 }; // 241 // 242 DiffSequence.applyChanges = function (doc, changeFields) { // 243 _.each(changeFields, function (value, key) { // 244 if (value === undefined) // 245 delete doc[key]; // 246 else // 247 doc[key] = value; // 248 }); // 249 }; // 250 // 251 // 252 ///////////////////////////////////////////////////////////////////////////////////// }).call(this); /* Exports */ if (typeof Package === 'undefined') Package = {}; Package['diff-sequence'] = { DiffSequence: DiffSequence }; })(); //# sourceMappingURL=diff-sequence.js.map