From aee54f3ac057c176c0ade7842a88db564a95e4b4 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 11 Dec 2012 22:30:28 -0800 Subject: [PATCH 1/6] Add skip and limit support to observe. --- packages/minimongo/minimongo.js | 33 ++++++++++++++++++++++++--- packages/minimongo/minimongo_tests.js | 19 +++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 91477e54df..34cbc22f9a 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -173,7 +173,6 @@ LocalCollection.LiveResultsSet = function () {}; // initial results delivered through added callback // XXX maybe callbacks should take a list of objects, to expose transactions? // XXX maybe support field limiting (to limit what you're notified on) -// XXX maybe support limit/skip _.extend(LocalCollection.Cursor.prototype, { observe: function (options) { @@ -187,8 +186,8 @@ _.extend(LocalCollection.Cursor.prototype, { _observeInternal: function (ordered, options) { var self = this; - if (self.skip || self.limit) - throw new Error("cannot observe queries with skip or limit"); + if (!ordered && (self.skip || self.limit)) + throw new Error("must use ordered observe with skip or limit"); var qid = self.collection.next_qid++; @@ -463,8 +462,16 @@ LocalCollection._deepcopy = function (v) { // XXX the sorted-query logic below is laughably inefficient. we'll // need to come up with a better datastructure for this. +// +// XXX the logic for observing with a skip or a limit is even more +// laughably inefficient. we recompute the whole results every time! LocalCollection._insertInResults = function (query, doc) { + if (query.cursor.skip || query.cursor.limit) { + LocalCollection._recomputeResults(query); + return; + } + if (query.ordered) { if (!query.sort_f) { query.added(LocalCollection._deepcopy(doc), query.results.length); @@ -481,6 +488,11 @@ LocalCollection._insertInResults = function (query, doc) { }; LocalCollection._removeFromResults = function (query, doc) { + if (query.cursor.skip || query.cursor.limit) { + LocalCollection._recomputeResults(query); + return; + } + if (query.ordered) { var i = LocalCollection._findInOrderedResults(query, doc); query.removed(doc, i); @@ -493,6 +505,11 @@ LocalCollection._removeFromResults = function (query, doc) { }; LocalCollection._updateInResults = function (query, doc, old_doc) { + if (query.cursor.skip || query.cursor.limit) { + LocalCollection._recomputeResults(query); + return; + } + if (doc._id !== old_doc._id) throw new Error("Can't change a doc's _id while updating"); @@ -517,6 +534,16 @@ LocalCollection._updateInResults = function (query, doc, old_doc) { query.moved(LocalCollection._deepcopy(doc), orig_idx, new_idx); }; +LocalCollection._recomputeResults = function (query, doc) { + var old_results = query.results; + query.results = query.cursor._getRawObjects(query.ordered); + + if (!query.paused) + LocalCollection._diffQuery( + query.ordered, old_results, query.results, query, true); +}; + + LocalCollection._findInOrderedResults = function (query, doc) { if (!query.ordered) throw new Error("Can't call _findInOrderedResults on unordered query"); diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index fdfe5b3c54..e9de875f7c 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -1203,6 +1203,25 @@ Tinytest.add("minimongo - observe", function (test) { c.insert({a:100}); test.equal(operations.shift(), ['added', {a:100}, 0]); handle.stop(); + + // test skip and limit. + c.remove({}); + handle = c.find({}, {sort: {a: 1}, skip: 1, limit: 2}).observe(cbs); + test.equal(operations.shift(), undefined); + c.insert({a:1}); + test.equal(operations.shift(), undefined); + c.insert({a:2}); + test.equal(operations.shift(), ['added', {a:2}, 0]); + c.insert({a:3}); + test.equal(operations.shift(), ['added', {a:3}, 1]); + c.insert({a:4}); + test.equal(operations.shift(), undefined); + id = c.findOne({a:2})._id; + c.update({a:1}, {a:5}); + test.equal(operations.shift(), ['removed', id, 0, {a:2}]); + test.equal(operations.shift(), ['added', {a:4}, 1]); + + handle.stop(); }); Tinytest.add("minimongo - diff", function (test) { From 0faaa07c777e59f9bad7d65ed3b2b4021db2c039 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Fri, 14 Dec 2012 18:52:27 -0800 Subject: [PATCH 2/6] Feedback from review. --- packages/minimongo/minimongo.js | 15 +++++++++++---- packages/minimongo/minimongo_tests.js | 4 +++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 34cbc22f9a..fa32a06623 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -94,10 +94,17 @@ LocalCollection.prototype.findOne = function (selector, options) { if (arguments.length === 0) selector = {}; - // XXX disable limit here so that we can observe findOne() cursor, - // as required by markAsReactive. - // options = options || {}; - // options.limit = 1; + // NOTE: by setting limit 1 here, we end up using very inefficient + // code that recomputes the whole query on each update. The upside is + // that when you reactively depend on a findOne you only get + // invalidated when the found object changes, not any object in the + // collection. Most findOne will be by id, which has a fast path, so + // this might not be a big deal. In most cases, invalidation causes + // the called to re-query anyway, so this should be a net performance + // improvement. + options = options || {}; + options.limit = 1; + return this.find(selector, options).fetch()[0]; }; diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index e9de875f7c..f0affd485b 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -1217,7 +1217,9 @@ Tinytest.add("minimongo - observe", function (test) { c.insert({a:4}); test.equal(operations.shift(), undefined); id = c.findOne({a:2})._id; - c.update({a:1}, {a:5}); + c.update({a:1}, {a:0}); + test.equal(operations.shift(), undefined); + c.update({a:0}, {a:5}); test.equal(operations.shift(), ['removed', id, 0, {a:2}]); test.equal(operations.shift(), ['added', {a:4}, 1]); From 2d9e8a8fd62a79069af0dfa1bf7ffb925a80ea17 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 11 Dec 2012 22:33:37 -0800 Subject: [PATCH 3/6] Move some tests around. No functional changes. --- packages/minimongo/minimongo_tests.js | 222 ++++++++++++++------------ 1 file changed, 116 insertions(+), 106 deletions(-) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index f0affd485b..777b8a73ec 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -59,123 +59,81 @@ var log_callbacks = function (operations) { }; // XXX test shared structure in all MM entrypoints +Tinytest.add("minimongo - basics", function (test) { + var c = new LocalCollection(); -_.each(['observe', '_observeUnordered'], function (observeMethod) { - Tinytest.add("minimongo - basics (" + observeMethod + ")", function (test) { - var c = new LocalCollection(); + c.insert({type: "kitten", name: "fluffy"}); + c.insert({type: "kitten", name: "snookums"}); + c.insert({type: "cryptographer", name: "alice"}); + c.insert({type: "cryptographer", name: "bob"}); + c.insert({type: "cryptographer", name: "cara"}); + test.equal(c.find().count(), 5); + test.equal(c.find({type: "kitten"}).count(), 2); + test.equal(c.find({type: "cryptographer"}).count(), 3); + test.length(c.find({type: "kitten"}).fetch(), 2); + test.length(c.find({type: "cryptographer"}).fetch(), 3); - c.insert({type: "kitten", name: "fluffy"}); - c.insert({type: "kitten", name: "snookums"}); - c.insert({type: "cryptographer", name: "alice"}); - c.insert({type: "cryptographer", name: "bob"}); - c.insert({type: "cryptographer", name: "cara"}); - test.equal(c.find().count(), 5); - test.equal(c.find({type: "kitten"}).count(), 2); - test.equal(c.find({type: "cryptographer"}).count(), 3); - test.length(c.find({type: "kitten"}).fetch(), 2); - test.length(c.find({type: "cryptographer"}).fetch(), 3); + c.remove({name: "cara"}); + test.equal(c.find().count(), 4); + test.equal(c.find({type: "kitten"}).count(), 2); + test.equal(c.find({type: "cryptographer"}).count(), 2); + test.length(c.find({type: "kitten"}).fetch(), 2); + test.length(c.find({type: "cryptographer"}).fetch(), 2); - c.remove({name: "cara"}); - test.equal(c.find().count(), 4); - test.equal(c.find({type: "kitten"}).count(), 2); - test.equal(c.find({type: "cryptographer"}).count(), 2); - test.length(c.find({type: "kitten"}).fetch(), 2); - test.length(c.find({type: "cryptographer"}).fetch(), 2); + c.update({name: "snookums"}, {$set: {type: "cryptographer"}}); + test.equal(c.find().count(), 4); + test.equal(c.find({type: "kitten"}).count(), 1); + test.equal(c.find({type: "cryptographer"}).count(), 3); + test.length(c.find({type: "kitten"}).fetch(), 1); + test.length(c.find({type: "cryptographer"}).fetch(), 3); - c.update({name: "snookums"}, {$set: {type: "cryptographer"}}); - test.equal(c.find().count(), 4); - test.equal(c.find({type: "kitten"}).count(), 1); - test.equal(c.find({type: "cryptographer"}).count(), 3); - test.length(c.find({type: "kitten"}).fetch(), 1); - test.length(c.find({type: "cryptographer"}).fetch(), 3); + c.remove(null); + c.remove(false); + c.remove(undefined); + test.equal(c.find().count(), 4); - c.remove(null); - c.remove(false); - c.remove(undefined); - test.equal(c.find().count(), 4); + c.remove({_id: null}); + c.remove({_id: false}); + c.remove({_id: undefined}); + c.remove(); + test.equal(c.find().count(), 4); - c.remove({_id: null}); - c.remove({_id: false}); - c.remove({_id: undefined}); - c.remove(); - test.equal(c.find().count(), 4); + c.remove({}); + test.equal(c.find().count(), 0); - c.remove({}); - test.equal(c.find().count(), 0); + c.insert({_id: 1, name: "strawberry", tags: ["fruit", "red", "squishy"]}); + c.insert({_id: 2, name: "apple", tags: ["fruit", "red", "hard"]}); + c.insert({_id: 3, name: "rose", tags: ["flower", "red", "squishy"]}); - c.insert({_id: 1, name: "strawberry", tags: ["fruit", "red", "squishy"]}); - c.insert({_id: 2, name: "apple", tags: ["fruit", "red", "hard"]}); - c.insert({_id: 3, name: "rose", tags: ["flower", "red", "squishy"]}); + test.equal(c.find({tags: "flower"}).count(), 1); + test.equal(c.find({tags: "fruit"}).count(), 2); + test.equal(c.find({tags: "red"}).count(), 3); + test.length(c.find({tags: "flower"}).fetch(), 1); + test.length(c.find({tags: "fruit"}).fetch(), 2); + test.length(c.find({tags: "red"}).fetch(), 3); - test.equal(c.find({tags: "flower"}).count(), 1); - test.equal(c.find({tags: "fruit"}).count(), 2); - test.equal(c.find({tags: "red"}).count(), 3); - test.length(c.find({tags: "flower"}).fetch(), 1); - test.length(c.find({tags: "fruit"}).fetch(), 2); - test.length(c.find({tags: "red"}).fetch(), 3); + test.equal(c.findOne(1).name, "strawberry"); + test.equal(c.findOne(2).name, "apple"); + test.equal(c.findOne(3).name, "rose"); + test.equal(c.findOne(4), undefined); + test.equal(c.findOne("abc"), undefined); + test.equal(c.findOne(undefined), undefined); - test.equal(c.findOne(1).name, "strawberry"); - test.equal(c.findOne(2).name, "apple"); - test.equal(c.findOne(3).name, "rose"); - test.equal(c.findOne(4), undefined); - test.equal(c.findOne("abc"), undefined); - test.equal(c.findOne(undefined), undefined); + test.equal(c.find(1).count(), 1); + test.equal(c.find(4).count(), 0); + test.equal(c.find("abc").count(), 0); + test.equal(c.find(undefined).count(), 0); + test.equal(c.find().count(), 3); - test.equal(c.find(1).count(), 1); - test.equal(c.find(4).count(), 0); - test.equal(c.find("abc").count(), 0); - test.equal(c.find(undefined).count(), 0); - test.equal(c.find().count(), 3); + // Regression test for #455. + c.insert({foo: {bar: 'baz'}}); + test.equal(c.find({foo: {bam: 'baz'}}).count(), 0); + test.equal(c.find({foo: {bar: 'baz'}}).count(), 1); - // Regression test for #455. - c.insert({foo: {bar: 'baz'}}); - test.equal(c.find({foo: {bam: 'baz'}}).count(), 0); - test.equal(c.find({foo: {bar: 'baz'}}).count(), 1); - - // Duplicate ID. - test.throws(function () { c.insert({_id: 1, name: "bla"}); }); - test.equal(c.find({_id: 1}).count(), 1); - test.equal(c.findOne(1).name, "strawberry"); - - var ev = ""; - var makecb = function (tag) { - return { - added: function (doc) { ev += "a" + tag + doc._id + "_"; }, - changed: function (doc) { ev += "c" + tag + doc._id + "_"; }, - removed: function (doc) { ev += "r" + tag + doc._id + "_"; } - }; - }; - var expect = function (x) { - test.equal(ev, x); - ev = ""; - }; - // This should work equally well for ordered and unordered observations - // (because the callbacks don't look at indices and there's no 'moved' - // callback). - var handle = c.find({tags: "flower"})[observeMethod](makecb('a')); - expect("aa3_"); - c.update({name: "rose"}, {$set: {tags: ["bloom", "red", "squishy"]}}); - expect("ra3_"); - c.update({name: "rose"}, {$set: {tags: ["flower", "red", "squishy"]}}); - expect("aa3_"); - c.update({name: "rose"}, {$set: {food: false}}); - expect("ca3_"); - c.remove({}); - expect("ra3_"); - c.insert({_id: 4, name: "daisy", tags: ["flower"]}); - expect("aa4_"); - handle.stop(); - // After calling stop, no more callbacks are called. - c.insert({_id: 5, name: "iris", tags: ["flower"]}); - expect(""); - - // Test that observing a lookup by ID works. - handle = c.find(4)[observeMethod](makecb('b')); - expect('ab4_'); - c.update(4, {$set: {eek: 5}}); - expect('cb4_'); - handle.stop(); - }); + // Duplicate ID. + test.throws(function () { c.insert({_id: 1, name: "bla"}); }); + test.equal(c.find({_id: 1}).count(), 1); + test.equal(c.findOne(1).name, "strawberry"); }); Tinytest.add("minimongo - cursors", function (test) { @@ -1159,7 +1117,7 @@ Tinytest.add("minimongo - modify", function (test) { // XXX test update() (selecting docs, multi, upsert..) -Tinytest.add("minimongo - observe", function (test) { +Tinytest.add("minimongo - observe ordered", function (test) { var operations = []; var cbs = log_callbacks(operations); var handle; @@ -1226,6 +1184,58 @@ Tinytest.add("minimongo - observe", function (test) { handle.stop(); }); +_.each(['observe', '_observeUnordered'], function (observeMethod) { + Tinytest.add("minimongo - observe (" + observeMethod + ")", function (test) { + var c = new LocalCollection(); + + var ev = ""; + var makecb = function (tag) { + return { + added: function (doc) { ev += "a" + tag + doc._id + "_"; }, + changed: function (doc) { ev += "c" + tag + doc._id + "_"; }, + removed: function (doc) { ev += "r" + tag + doc._id + "_"; } + }; + }; + var expect = function (x) { + test.equal(ev, x); + ev = ""; + }; + + c.insert({_id: 1, name: "strawberry", tags: ["fruit", "red", "squishy"]}); + c.insert({_id: 2, name: "apple", tags: ["fruit", "red", "hard"]}); + c.insert({_id: 3, name: "rose", tags: ["flower", "red", "squishy"]}); + + // This should work equally well for ordered and unordered observations + // (because the callbacks don't look at indices and there's no 'moved' + // callback). + var handle = c.find({tags: "flower"})[observeMethod](makecb('a')); + expect("aa3_"); + c.update({name: "rose"}, {$set: {tags: ["bloom", "red", "squishy"]}}); + expect("ra3_"); + c.update({name: "rose"}, {$set: {tags: ["flower", "red", "squishy"]}}); + expect("aa3_"); + c.update({name: "rose"}, {$set: {food: false}}); + expect("ca3_"); + c.remove({}); + expect("ra3_"); + c.insert({_id: 4, name: "daisy", tags: ["flower"]}); + expect("aa4_"); + handle.stop(); + // After calling stop, no more callbacks are called. + c.insert({_id: 5, name: "iris", tags: ["flower"]}); + expect(""); + + // Test that observing a lookup by ID works. + handle = c.find(4)[observeMethod](makecb('b')); + expect('ab4_'); + c.update(4, {$set: {eek: 5}}); + expect('cb4_'); + handle.stop(); + }); +}); + + + Tinytest.add("minimongo - diff", function (test) { // test correctness From 33fa24e912ba61796aade4432644286741016cf4 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Fri, 14 Dec 2012 19:50:32 -0800 Subject: [PATCH 4/6] Make find(1) and find({_id: 1}) return the same thing. --- packages/minimongo/minimongo.js | 10 ++++++++-- packages/minimongo/minimongo_tests.js | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index fa32a06623..366f2d478c 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -70,9 +70,9 @@ LocalCollection.Cursor = function (collection, selector, options) { } else { this.selector_f = LocalCollection._compileSelector(selector); this.sort_f = options.sort ? LocalCollection._compileSort(options.sort) : null; - this.skip = options.skip; - this.limit = options.limit; } + this.skip = options.skip; + this.limit = options.limit; // db_objects is a list of the objects that match the cursor. (It's always a // list, never an object: LocalCollection.Cursor is always ordered.) @@ -260,6 +260,12 @@ LocalCollection.Cursor.prototype._getRawObjects = function (ordered) { // fast path for single ID value if (self.selector_id) { + // If you have non-zero skip and ask for a single id, you get + // nothing. This is so it matches the behavior of the '{_id: foo}' + // path. + if (self.skip) + return results; + if (_.has(self.collection.docs, self.selector_id)) { var selectedDoc = self.collection.docs[self.selector_id]; if (ordered) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 777b8a73ec..b3713eb25c 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -124,6 +124,8 @@ Tinytest.add("minimongo - basics", function (test) { test.equal(c.find("abc").count(), 0); test.equal(c.find(undefined).count(), 0); test.equal(c.find().count(), 3); + test.equal(c.find(1, {skip: 1}).count(), 0); + test.equal(c.find({_id: 1}, {skip: 1}).count(), 0); // Regression test for #455. c.insert({foo: {bar: 'baz'}}); From ac81438f74ceb5f801c76c55441c8813a03a5948 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Sat, 15 Dec 2012 17:32:25 -0800 Subject: [PATCH 5/6] Refinements to limit/skip observe support: - Decide that queries need to be recomputed in _modifyAndNotify directly instead of in _removeFromResults and friends; this is because the choice of which _fooFromResults function to invoke might not actually be correct in the presence of skip and limit. (The previous code still worked because all three of those functions had the same code for the skip/limit case.) - If update or remove affects more than one doc for a query, only recompute it once at the end instead of once per doc. --- packages/minimongo/minimongo.js | 72 +++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 366f2d478c..f84aad0674 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -204,7 +204,8 @@ _.extend(LocalCollection.Cursor.prototype, { sort_f: ordered && self.sort_f, results_snapshot: null, ordered: ordered, - cursor: this + cursor: this, + needsRecompute: false }; query.results = self._getRawObjects(ordered); if (self.collection.paused) @@ -349,9 +350,15 @@ LocalCollection.prototype.insert = function (doc) { // trigger live queries that match for (var qid in self.queries) { var query = self.queries[qid]; - if (query.selector_f(doc)) - LocalCollection._insertInResults(query, doc); + if (query.selector_f(doc)) { + if (query.cursor.skip || query.cursor.limit) + query.needsRecompute = true; + else + LocalCollection._insertInResults(query, doc); + } } + + self._recomputeNecessaryQueries(); }; LocalCollection.prototype.remove = function (selector) { @@ -377,8 +384,12 @@ LocalCollection.prototype.remove = function (selector) { var removeId = remove[i]; var removeDoc = self.docs[removeId]; _.each(self.queries, function (query) { - if (query.selector_f(removeDoc)) - queryRemove.push([query, removeDoc]); + if (query.selector_f(removeDoc)) { + if (query.cursor.skip || query.cursor.limit) + query.needsRecompute = true; + else + queryRemove.push([query, removeDoc]); + } }); self._saveOriginal(removeId, removeDoc); delete self.docs[removeId]; @@ -388,6 +399,8 @@ LocalCollection.prototype.remove = function (selector) { for (var i = 0; i < queryRemove.length; i++) { LocalCollection._removeFromResults(queryRemove[i][0], queryRemove[i][1]); } + + self._recomputeNecessaryQueries(); }; // XXX atomicity: if multi is true, and one modification fails, do @@ -405,7 +418,7 @@ LocalCollection.prototype.update = function (selector, mod, options) { self._saveOriginal(id, doc); self._modifyAndNotify(doc, mod); if (!options.multi) - return; + break; any = true; } } @@ -420,6 +433,8 @@ LocalCollection.prototype.update = function (selector, mod, options) { self.insert(insert); } } + + self._recomputeNecessaryQueries(); }; LocalCollection.prototype._modifyAndNotify = function (doc, mod) { @@ -443,12 +458,24 @@ LocalCollection.prototype._modifyAndNotify = function (doc, mod) { query = self.queries[qid]; var before = matched_before[qid]; var after = query.selector_f(doc); - if (before && !after) + + if (query.cursor.skip || query.cursor.limit) { + // We need to recompute any query where the doc may have been in the + // cursor's window either before or after the update. (Note that if skip + // or limit is set, "before" and "after" being true do not necessarily + // mean that the document is in the cursor's output after skip/limit is + // applied... but if they are false, then the document definitely is NOT + // in the output. So it's safe to skip recompute if neither before or + // after are true.) + if (before || after) + query.needsRecompute = true; + } else if (before && !after) { LocalCollection._removeFromResults(query, doc); - else if (!before && after) + } else if (!before && after) { LocalCollection._insertInResults(query, doc); - else if (before && after) + } else if (before && after) { LocalCollection._updateInResults(query, doc, old_doc); + } } }; @@ -480,11 +507,6 @@ LocalCollection._deepcopy = function (v) { // laughably inefficient. we recompute the whole results every time! LocalCollection._insertInResults = function (query, doc) { - if (query.cursor.skip || query.cursor.limit) { - LocalCollection._recomputeResults(query); - return; - } - if (query.ordered) { if (!query.sort_f) { query.added(LocalCollection._deepcopy(doc), query.results.length); @@ -501,11 +523,6 @@ LocalCollection._insertInResults = function (query, doc) { }; LocalCollection._removeFromResults = function (query, doc) { - if (query.cursor.skip || query.cursor.limit) { - LocalCollection._recomputeResults(query); - return; - } - if (query.ordered) { var i = LocalCollection._findInOrderedResults(query, doc); query.removed(doc, i); @@ -518,11 +535,6 @@ LocalCollection._removeFromResults = function (query, doc) { }; LocalCollection._updateInResults = function (query, doc, old_doc) { - if (query.cursor.skip || query.cursor.limit) { - LocalCollection._recomputeResults(query); - return; - } - if (doc._id !== old_doc._id) throw new Error("Can't change a doc's _id while updating"); @@ -547,7 +559,17 @@ LocalCollection._updateInResults = function (query, doc, old_doc) { query.moved(LocalCollection._deepcopy(doc), orig_idx, new_idx); }; -LocalCollection._recomputeResults = function (query, doc) { +LocalCollection.prototype._recomputeNecessaryQueries = function () { + var self = this; + _.each(self.queries, function (query) { + if (query.needsRecompute) { + LocalCollection._recomputeResults(query); + query.needsRecompute = false; + } + }); +}; + +LocalCollection._recomputeResults = function (query) { var old_results = query.results; query.results = query.cursor._getRawObjects(query.ordered); From e32502c20c48b602bc437853f69787d0b51c6276 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 17 Dec 2012 09:53:21 -0800 Subject: [PATCH 6/6] Add a fast path to limited unsorted unskipped queries. --- packages/minimongo/minimongo.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index f84aad0674..0e328253c0 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -286,6 +286,10 @@ LocalCollection.Cursor.prototype._getRawObjects = function (ordered) { else results[id] = doc; } + // Fast path for limited unsorted queries. + if (self.limit && !self.skip && !self.sort_f && + results.length === self.limit) + return results; } if (!ordered)