1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/rocketchat_ynh.git synced 2024-09-03 20:16:25 +02:00
rocketchat_ynh/sources/programs/server/packages/diff-sequence.js
2016-04-29 16:32:48 +02:00

284 lines
23 KiB
JavaScript

(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<N; i++) { // 124
if (old_index_of_id[new_results[i]._id] !== undefined) { // 125
var j = max_seq_len; // 126
// this inner loop would traditionally be a binary search, // 127
// but scanning backwards we will likely find a subseq to extend // 128
// pretty soon, bounded for example by the total number of ops. // 129
// If this were to be changed to a binary search, we'd still want // 130
// to scan backwards a bit as an optimization. // 131
while (j > 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