diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 34b5efa035..71977c86bf 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -42,7 +42,7 @@ LocalCollection = function (options) { // ordered: bool. ordered queries have addedBefore/movedBefore callbacks. // results: array (ordered) or object (unordered) of current results // (aliased with self._docs!) - // results_snapshot: snapshot of results. null if not paused. + // resultsSnapshot: snapshot of results. null if not paused. // cursor: Cursor object for the query. // selector, sorter, (callbacks): functions self.queries = {}; @@ -128,7 +128,7 @@ LocalCollection.Cursor = function (collection, selector, options) { self.fields = options.fields; if (self.fields) - self.projection_f = LocalCollection._compileProjection(self.fields); + self.projectionFn = LocalCollection._compileProjection(self.fields); self._transform = LocalCollection.wrapTransform(options.transform); @@ -170,7 +170,7 @@ LocalCollection.Cursor.prototype.forEach = function (callback, thisArg) { var self = this; if (self.db_objects === null) - self.db_objects = self._getRawObjects(true); + self.db_objects = self._getRawObjects({ordered: true}); if (self.reactive) self._depend({ @@ -181,8 +181,8 @@ LocalCollection.Cursor.prototype.forEach = function (callback, thisArg) { while (self.cursor_pos < self.db_objects.length) { var elt = EJSON.clone(self.db_objects[self.cursor_pos]); - if (self.projection_f) - elt = self.projection_f(elt); + if (self.projectionFn) + elt = self.projectionFn(elt); if (self._transform) elt = self._transform(elt); callback.call(thisArg, elt, self.cursor_pos, self); @@ -220,7 +220,7 @@ LocalCollection.Cursor.prototype.count = function () { true /* allow the observe to be unordered */); if (self.db_objects === null) - self.db_objects = self._getRawObjects(true); + self.db_objects = self._getRawObjects({ordered: true}); return self.db_objects.length; }; @@ -302,12 +302,10 @@ _.extend(LocalCollection.Cursor.prototype, { sorter: ordered && self.sorter, distances: ( self.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap), - results_snapshot: null, + resultsSnapshot: null, ordered: ordered, cursor: self, - observeChanges: options.observeChanges, - fields: self.fields, - projection_f: self.projection_f + projectionFn: self.projectionFn }; var qid; @@ -317,9 +315,10 @@ _.extend(LocalCollection.Cursor.prototype, { qid = self.collection.next_qid++; self.collection.queries[qid] = query; } - query.results = self._getRawObjects(ordered, query.distances); + query.results = self._getRawObjects({ + ordered: ordered, distances: query.distances}); if (self.collection.paused) - query.results_snapshot = (ordered ? [] : new LocalCollection._IdMap); + query.resultsSnapshot = (ordered ? [] : new LocalCollection._IdMap); // wrap callbacks we were passed. callbacks only fire when not paused and // are never undefined @@ -335,17 +334,18 @@ _.extend(LocalCollection.Cursor.prototype, { var context = this; var args = arguments; - if (fieldsIndex !== undefined && self.projection_f) { - args[fieldsIndex] = self.projection_f(args[fieldsIndex]); + if (self.collection.paused) + return; + + if (fieldsIndex !== undefined && self.projectionFn) { + args[fieldsIndex] = self.projectionFn(args[fieldsIndex]); if (ignoreEmptyFields && _.isEmpty(args[fieldsIndex])) return; } - if (!self.collection.paused) { - self.collection._observeQueue.queueTask(function () { - f.apply(context, args); - }); - } + self.collection._observeQueue.queueTask(function () { + f.apply(context, args); + }); }; }; query.added = wrapCallback(options.added, 1); @@ -413,13 +413,13 @@ _.extend(LocalCollection.Cursor.prototype, { // argument, this function will clear it and use it for this purpose (otherwise // it will just create its own _IdMap). The observeChanges implementation uses // this to remember the distances after this function returns. -LocalCollection.Cursor.prototype._getRawObjects = function (ordered, - distances) { +LocalCollection.Cursor.prototype._getRawObjects = function (options) { var self = this; + options = options || {}; // XXX use OrderedDict instead of array, and make IdMap and OrderedDict // compatible - var results = ordered ? [] : new LocalCollection._IdMap; + var results = options.ordered ? [] : new LocalCollection._IdMap; // fast path for single ID value if (self._selectorId !== undefined) { @@ -431,7 +431,7 @@ LocalCollection.Cursor.prototype._getRawObjects = function (ordered, var selectedDoc = self.collection._docs.get(self._selectorId); if (selectedDoc) { - if (ordered) + if (options.ordered) results.push(selectedDoc); else results.set(self._selectorId, selectedDoc); @@ -444,17 +444,20 @@ LocalCollection.Cursor.prototype._getRawObjects = function (ordered, // in the observeChanges case, distances is actually part of the "query" (ie, // live results set) object. in other cases, distances is only used inside // this function. - if (self.matcher.hasGeoQuery() && ordered) { - if (distances) + var distances; + if (self.matcher.hasGeoQuery() && options.ordered) { + if (options.distances) { + distances = options.distances; distances.clear(); - else + } else { distances = new LocalCollection._IdMap(); + } } self.collection._docs.forEach(function (doc, id) { var matchResult = self.matcher.documentMatches(doc); if (matchResult.result) { - if (ordered) { + if (options.ordered) { results.push(doc); if (distances && matchResult.distance !== undefined) distances.set(id, matchResult.distance); @@ -470,7 +473,7 @@ LocalCollection.Cursor.prototype._getRawObjects = function (ordered, return true; // continue }); - if (!ordered) + if (!options.ordered) return results; if (self.sorter) { @@ -560,31 +563,60 @@ LocalCollection.prototype.insert = function (doc, callback) { return id; }; -LocalCollection.prototype.remove = function (selector, callback) { +// Iterates over a subset of documents that could match selector; calls +// f(doc, id) on each of them. Specifically, if selector specifies +// specific _id's, it only looks at those. doc is *not* cloned: it is the +// same object that is in _docs. +LocalCollection.prototype._eachPossiblyMatchingDoc = function (selector, f) { var self = this; - var remove = []; - - var queriesToRecompute = []; - var matcher = new Minimongo.Matcher(selector, self); - - // Avoid O(n) for "remove a single doc by ID". var specificIds = LocalCollection._idsMatchedBySelector(selector); if (specificIds) { - _.each(specificIds, function (id) { - // We still have to run matcher, in case it's something like - // {_id: "X", a: 42} + for (var i = 0; i < specificIds.length; ++i) { + var id = specificIds[i]; var doc = self._docs.get(id); - if (doc && matcher.documentMatches(doc).result) - remove.push(id); - }); + if (doc) { + var breakIfFalse = f(doc, id); + if (breakIfFalse === false) + break; + } + } } else { - self._docs.forEach(function (doc, id) { - if (matcher.documentMatches(doc).result) { - remove.push(id); + self._docs.forEach(f); + } +}; + +LocalCollection.prototype.remove = function (selector, callback) { + var self = this; + + // Easy special case: if we're not calling observeChanges callbacks and we're + // not saving originals and we got asked to remove everything, then just empty + // everything directly. + if (self.paused && !self._savedOriginals && EJSON.equals(selector, {})) { + var result = self._docs.size(); + self._docs.clear(); + _.each(self.queries, function (query) { + if (query.ordered) { + query.results = []; + } else { + query.results.clear(); } }); + if (callback) { + Meteor.defer(function () { + callback(null, result); + }); + } + return result; } + var matcher = new Minimongo.Matcher(selector, self); + var remove = []; + self._eachPossiblyMatchingDoc(selector, function (doc, id) { + if (matcher.documentMatches(doc).result) + remove.push(id); + }); + + var queriesToRecompute = []; var queryRemove = []; for (var i = 0; i < remove.length; i++) { var removeId = remove[i]; @@ -615,7 +647,7 @@ LocalCollection.prototype.remove = function (selector, callback) { LocalCollection._recomputeResults(query); }); self._observeQueue.drain(); - var result = remove.length; + result = remove.length; if (callback) Meteor.defer(function () { callback(null, result); @@ -638,7 +670,7 @@ LocalCollection.prototype.update = function (selector, mod, options, callback) { // Save the original results of any query that we might need to // _recomputeResults on, because _modifyAndNotify will mutate the objects in // it. (We don't need to save the original results of paused queries because - // they already have a results_snapshot and we won't be diffing in + // they already have a resultsSnapshot and we won't be diffing in // _recomputeResults.) var qidToOriginalResults = {}; _.each(self.queries, function (query, qid) { @@ -651,7 +683,7 @@ LocalCollection.prototype.update = function (selector, mod, options, callback) { var updateCount = 0; - self._docs.forEach(function (doc, id) { + self._eachPossiblyMatchingDoc(selector, function (doc, id) { var queryResult = matcher.documentMatches(doc); if (queryResult.result) { // XXX Should we save the original even if mod ends up being a no-op? @@ -860,7 +892,8 @@ LocalCollection._recomputeResults = function (query, oldResults) { oldResults = query.results; if (query.distances) query.distances.clear(); - query.results = query.cursor._getRawObjects(query.ordered, query.distances); + query.results = query.cursor._getRawObjects({ + ordered: query.ordered, distances: query.distances}); if (!query.paused) { LocalCollection._diffQueryChanges( @@ -956,7 +989,7 @@ LocalCollection.prototype.pauseObservers = function () { for (var qid in this.queries) { var query = this.queries[qid]; - query.results_snapshot = EJSON.clone(query.results); + query.resultsSnapshot = EJSON.clone(query.results); } }; @@ -979,8 +1012,8 @@ LocalCollection.prototype.resumeObservers = function () { // Diff the current results against the snapshot and send to observers. // pass the query object for its observer callbacks. LocalCollection._diffQueryChanges( - query.ordered, query.results_snapshot, query.results, query); - query.results_snapshot = null; + query.ordered, query.resultsSnapshot, query.results, query); + query.resultsSnapshot = null; } self._observeQueue.drain(); }; diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 68c6b0203d..4bb58d2b94 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -2170,7 +2170,8 @@ Tinytest.add("minimongo - observe ordered", function (test) { handle.stop(); // test _suppress_initial - handle = c.find({}, {sort: {a: -1}}).observe(_.extend(cbs, {_suppress_initial: true})); + handle = c.find({}, {sort: {a: -1}}).observe(_.extend({ + _suppress_initial: true}, cbs)); test.equal(operations.shift(), undefined); c.insert({a:100}); test.equal(operations.shift(), ['added', {a:100}, 0, idA2]); @@ -2197,6 +2198,21 @@ Tinytest.add("minimongo - observe ordered", function (test) { test.equal(operations.shift(), ['changed', {a:3.5}, 0, {a:3}]); handle.stop(); + // test observe limit with pre-existing docs + c.remove({}); + c.insert({a: 1}); + c.insert({_id: 'two', a: 2}); + c.insert({a: 3}); + handle = c.find({}, {sort: {a: 1}, limit: 2}).observe(cbs); + test.equal(operations.shift(), ['added', {a:1}, 0, null]); + test.equal(operations.shift(), ['added', {a:2}, 1, null]); + test.equal(operations.shift(), undefined); + c.remove({a: 2}); + test.equal(operations.shift(), ['removed', 'two', 1, {a:2}]); + test.equal(operations.shift(), ['added', {a:3}, 1, null]); + test.equal(operations.shift(), undefined); + handle.stop(); + // test _no_indices c.remove({}); @@ -2565,6 +2581,14 @@ Tinytest.add("minimongo - pause", function (test) { test.equal(operations.shift(), ['changed', {a:3}, 0, {a:1}]); test.length(operations, 0); + // test special case for remove({}) + c.pauseObservers(); + test.equal(c.remove({}), 1); + test.length(operations, 0); + c.resumeObservers(); + test.equal(operations.shift(), ['removed', 1, 0, {a:3}]); + test.length(operations, 0); + h.stop(); }); diff --git a/packages/underscore/underscore.js b/packages/underscore/underscore.js index 12a74d5f55..3ffa3c111f 100644 --- a/packages/underscore/underscore.js +++ b/packages/underscore/underscore.js @@ -70,6 +70,21 @@ // Collection Functions // -------------------- + // METEOR CHANGE: Define _isArguments instead of depending on + // _.isArguments which is defined using each. In looksLikeArray + // (which each depends on), we then use _isArguments instead of + // _.isArguments. + var _isArguments = function (obj) { + return toString.call(obj) === '[object Arguments]'; + }; + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. + if (!_isArguments(arguments)) { + _isArguments = function (obj) { + return !!(obj && hasOwnProperty.call(obj, 'callee')); + }; + } + // METEOR CHANGE: _.each({length: 5}) should be treated like an object, not an // array. This looksLikeArray function is introduced by Meteor, and replaces // all instances of `obj.length === +obj.length`. @@ -77,7 +92,8 @@ // https://github.com/jashkenas/underscore/issues/770 var looksLikeArray = function (obj) { return (obj.length === +obj.length - && (_.isArguments(obj) || obj.constructor !== Object)); + // _.isArguments not yet necessarily defined here + && (_isArguments(obj) || obj.constructor !== Object)); }; // The cornerstone, an `each` implementation, aka `forEach`.