diff --git a/app/meteor/skel/client/~name~.js b/app/meteor/skel/client/~name~.js
index 3fb60df8dd..58c4b81eb4 100644
--- a/app/meteor/skel/client/~name~.js
+++ b/app/meteor/skel/client/~name~.js
@@ -7,9 +7,9 @@ Template.button_demo.events = {
};
Template.button_demo.ever_pressed = function (options) {
- return Clicks.find().length > 0;
+ return Clicks.find().count() > 0;
};
Template.button_demo.press_count = function () {
- return Clicks.find().length;
+ return Clicks.find().count();
};
diff --git a/examples/leaderboard/leaderboard.js b/examples/leaderboard/leaderboard.js
index a1eee8c3fb..53e5a82b8f 100644
--- a/examples/leaderboard/leaderboard.js
+++ b/examples/leaderboard/leaderboard.js
@@ -13,8 +13,7 @@ if (Meteor.is_client) {
$(document).ready(function () {
// List the players by score. You can click to select a player.
- var scores = Meteor.ui.renderList(Players, {
- sort: {score: -1}, // sort from high to low score
+ var scores = Meteor.ui.renderList(Players.find({}, {sort: {score: -1}}), {
render: function (player) {
if (Session.equals("selected_player", player._id))
var style = "player selected";
@@ -40,7 +39,7 @@ if (Meteor.is_client) {
if (!selected_player)
return $('
Click a player to select
');
- var player = Players.find(selected_player);
+ var player = Players.findOne(selected_player);
return $('');
}, {
@@ -60,12 +59,13 @@ if (Meteor.is_client) {
if (Meteor.is_server) {
// Publish the top 10 players, live, to any client that wants them.
- Meteor.publish("top10", {collection: Players, sort: {score: -1},
- limit: 10});
+ Meteor.publish("top10", {collection: Players,
+ sort: {score: -1},
+ limit: 10});
// On server startup, create some players if the database is empty.
Meteor.startup(function () {
- if (Players.find().length === 0) {
+ if (Players.find().count() === 0) {
var names = ["Glinnes Hulden", "Shira Hulden", "Denzel Warhound",
"Lute Casagave", "Akadie", "Thammas, Lord Gensifer",
"Ervil Savat", "Duissane Trevanyi", "Sagmondo Bandolio",
diff --git a/examples/todos/client/todos.js b/examples/todos/client/todos.js
index d405375192..5734816dda 100644
--- a/examples/todos/client/todos.js
+++ b/examples/todos/client/todos.js
@@ -8,8 +8,8 @@ Meteor.subscribe('lists', {}, function () {
// Once the lists have loaded, select the first one.
if (!Session.get('list_id')) {
var lists = Lists.find({}, {sort: {name: 1}, limit: 1});
- if (lists.length)
- Router.setList(lists[0]._id);
+ if (lists.count() > 0)
+ Router.setList(lists.get(0)._id);
}
});
@@ -23,7 +23,7 @@ Meteor.autosubscribe(function () {
Template.tag_filter.tags = function () {
// Pick out the unique tags from all tasks.
- var tags = _(Todos.find())
+ var tags = _(Todos.find().fetch())
.chain().pluck('tags').compact().flatten().sort().uniq(true).value();
// for some reason, .map can't be chained on IE8. underscore bug?
tags = _.map(tags, function (tag) { return {tag: tag} });
diff --git a/examples/todos/server/bootstrap.js b/examples/todos/server/bootstrap.js
index b1fa1a88ee..394e1ec27c 100644
--- a/examples/todos/server/bootstrap.js
+++ b/examples/todos/server/bootstrap.js
@@ -1,6 +1,6 @@
// if the database is empty on server start, create some sample data.
Meteor.startup(function () {
- if (Lists.find().length === 0) {
+ if (Lists.find().count() === 0) {
var data = [
{name: "* Seven Principles *",
contents: [
diff --git a/packages/livedata/livedata_client.js b/packages/livedata/livedata_client.js
index 1507078948..caffc0cc5b 100644
--- a/packages/livedata/livedata_client.js
+++ b/packages/livedata/livedata_client.js
@@ -26,7 +26,7 @@ if (typeof Meteor === "undefined") Meteor = {};
// XXX this is all a little whack. Need to think about how we handle
// removes, etc.
_.each(changes.inserted || [], function (elt) {
- if (!coll.find(elt._id)) {
+ if (!coll.findOne(elt._id)) {
coll._collection.insert(elt);
} else {
// we already added it locally! this is the case after an insert
@@ -58,7 +58,7 @@ if (typeof Meteor === "undefined") Meteor = {};
// add new subscriptions at the end. this way they take effect after
// the handlers and we don't see flicker.
- _.each(subs.find(), function (sub) {
+ subs.find().forEach(function (sub) {
msg_list.push(
['subscribe', {
_id: sub._id, name: sub.name, args: sub.args}]);
@@ -73,7 +73,7 @@ if (typeof Meteor === "undefined") Meteor = {};
});
- var subsToken = subs.findLive({}, {
+ var subsToken = subs.find({}).observe({
added: function (sub) {
Meteor._stream.emit('subscribe', {
_id: sub._id, name: sub.name, args: sub.args});
@@ -100,6 +100,14 @@ if (typeof Meteor === "undefined") Meteor = {};
_name: name,
_collection: new Collection(),
+ find: function (selector, options) {
+ return this._collection.find(selector, options);
+ },
+
+ findOne: function (selector) {
+ return this._collection.findOne(selector);
+ },
+
insert: function (obj) {
// Generate an id for the object.
// XXX mutates the object passed in. that is not cool.
@@ -117,14 +125,6 @@ if (typeof Meteor === "undefined") Meteor = {};
return obj;
},
- find: function (selector, options) {
- return this._collection.find(selector, options);
- },
-
- findLive: function (selector, options) {
- return this._collection.findLive(selector, options);
- },
-
update: function (selector, mutator, options) {
if (typeof(selector) === "string")
selector = {_id: selector};
@@ -214,7 +214,7 @@ if (typeof Meteor === "undefined") Meteor = {};
// return an object with a stop method.
var token = {stop: function () {
if (!id) return; // must have an id (local from above).
- // just update the database. findLive takes care of the rest.
+ // just update the database. observe takes care of the rest.
subs.update({_id: id}, {$inc: {count: -1}});
}};
diff --git a/packages/livedata/livedata_server.js b/packages/livedata/livedata_server.js
index 574d700aad..1ffab6bf6a 100644
--- a/packages/livedata/livedata_server.js
+++ b/packages/livedata/livedata_server.js
@@ -227,7 +227,7 @@ if (typeof Meteor === "undefined") Meteor = {};
sort: opt("sort"),
skip: opt("skip"),
limit: opt("limit")
- }));
+ }).fetch());
};
publishes[name] = func;
@@ -255,6 +255,15 @@ if (typeof Meteor === "undefined") Meteor = {};
// and minimongo diverge. we should track each of those down and
// kill it.
+ find: function (selector, options) {
+ var cursor = new Meteor._mongo_driver.Cursor(this._name, selector, options);
+ return cursor;
+ },
+
+ findOne: function (selector, options) {
+ return this.find(selector, options).get(0);
+ },
+
insert: function (doc) {
// do id allocation here, so we never end up with an ObjectID.
// This only happens if some calls this directly on the server,
@@ -274,14 +283,6 @@ if (typeof Meteor === "undefined") Meteor = {};
return doc;
},
- find: function (selector, options) {
- return Meteor._mongo_driver.find(this._name, selector, options);
- },
-
- findLive: function () {
- throw new Error("findLive isn't supported on the server");
- },
-
update: function (selector, mod, options) {
return Meteor._mongo_driver.update(this._name, selector, mod, options);
},
@@ -308,4 +309,10 @@ if (typeof Meteor === "undefined") Meteor = {};
return ret;
};
+ Meteor.Collection.Query = function (collection, selector, options) {
+ if (!options) options = {};
+
+ this.cursor = Meteor._mongo_driver.find(this._name, selector, options);
+ };
+
})();
diff --git a/packages/livedata/mongo_driver.js b/packages/livedata/mongo_driver.js
index aa21fe61dc..60df033a44 100644
--- a/packages/livedata/mongo_driver.js
+++ b/packages/livedata/mongo_driver.js
@@ -51,8 +51,7 @@ function withCollection (collection_name, callback) {
//////////// Public API //////////
-
-function find (collection_name, selector, options) {
+function Cursor (collection_name, selector, options) {
var future = new Future;
withCollection(collection_name, function(err, collection) {
// XXX err handling
@@ -76,15 +75,75 @@ function find (collection_name, selector, options) {
if (options && options.skip)
cursor = cursor.skip(options.skip);
- var func = single_result ? cursor.nextObject : cursor.toArray;
- func.call(cursor, function(err, result) {
- // XXX error handling!
- future.return(result);
- });
+ future.return(cursor);
+ });
+ this.cursor = future.wait();
+};
+
+Cursor.prototype.forEach = function (callback) {
+ var self = this;
+ var future = new Future;
+
+ self.cursor.each(function (err, doc) {
+ if (err || !doc)
+ future.return(err);
+ else
+ callback(null, doc);
});
return future.wait();
};
+Cursor.prototype.map = function (callback) {
+ var self = this;
+ var res = [];
+ self.forEach(function (doc) {
+ res.push(callback(doc));
+ });
+ return res;
+};
+
+Cursor.prototype.rewind = function () {
+ var self = this;
+
+ self.cursor.rewind();
+};
+
+Cursor.prototype.fetch = function (length) {
+ var self = this;
+ var future = new Future;
+ var res = [];
+
+ self.cursor.each(function (err, doc) {
+ if (err)
+ future.return(err);
+ else if (!doc)
+ future.return(res);
+
+ res.push(doc);
+
+ if (length && res.length === length)
+ // immediately return w/o consuming another doc in the iterator
+ future.return(res);
+ });
+
+ return future.wait();
+};
+
+Cursor.prototype.get = function (i) {
+ var self = this;
+ return self.fetch(i + 1)[0];
+};
+
+Cursor.prototype.count = function () {
+ var self = this;
+ var future = new Future;
+
+ self.cursor.count(function (err, res) {
+ future.return(res);
+ });
+
+ return future.wait();
+};
function insert (collection_name, document) {
var future = new Future;
@@ -125,7 +184,6 @@ function remove (collection_name, selector) {
return future.wait();
};
-
function update (collection_name, selector, mod, options) {
var future = new Future;
// XXX this blocks for the operation to complete (safe:true), because
@@ -140,7 +198,6 @@ function update (collection_name, selector, mod, options) {
if (typeof(options.multi) === "undefined")
options.multi = true
-
withCollection(collection_name, function(err, collection) {
// XXX err handling
@@ -159,13 +216,13 @@ function update (collection_name, selector, mod, options) {
};
Meteor._mongo_driver = {
- find: find,
+ Cursor: Cursor,
insert: insert,
remove: remove,
update: update
};
// start database
-init(__meteor_bootstrap__.mongo_url)
+init(__meteor_bootstrap__.mongo_url);
})();
diff --git a/packages/liveui/liveui.js b/packages/liveui/liveui.js
index fade647695..de2eb7c6d4 100644
--- a/packages/liveui/liveui.js
+++ b/packages/liveui/liveui.js
@@ -148,12 +148,6 @@ Meteor.ui.render = function (render_func, events, event_data) {
/// (specific to mongo at the moment, but will be generalized
/// eventually.)
///
-/// Undocumented elsewhere: you may pass in a findLive handle instead
-/// of 'what'. In that case, we will use that query instead, and we
-/// will take responsibility for calling stop() on it! Let's leave
-/// undocumented for now, and document when the new database API
-/// lands.
-///
/// Exact GC semantics:
/// - Slow path: When a database change happens, unconditionally
/// update the rendering, but also schedule an onscreen check to
@@ -167,11 +161,17 @@ Meteor.ui.render = function (render_func, events, event_data) {
/// to only do the teardown if it is in fact still offscreen at
/// flush()-time.
/// https://app.asana.com/0/159908330244/382690197728
-Meteor.ui.renderList = function (what, options) {
+Meteor.ui.renderList = function (query, options) {
var outer_frag;
var outer_range;
var entry_ranges = [];
+ // protect against old invocations passing in a Collection or a
+ // LiveResultsSet.
+ // XXX remove in a few releases
+ if (!(query instanceof Collection.Query))
+ throw new Error("insert_before: at least one entry must exist");
+
// create the top-level document fragment/range that will be
// returned by renderList. called exactly once, ever (and that call
// will be before renderList returns.) returns nothing, sets
@@ -192,7 +192,7 @@ Meteor.ui.renderList = function (what, options) {
return;
}
- query.stop();
+ live_results.stop();
if (!old_context.killed)
Meteor.ui._cleanup(outer_range);
};
@@ -360,9 +360,9 @@ Meteor.ui.renderList = function (what, options) {
// the screen. (The user has from when we're created, to when
// flush() is called, to put us on the screen.)
outer_range.context.invalidate();
- }
+ };
- var query_opts = {
+ var observe_callbacks = {
added: function (doc, before_idx) {
check_onscreen();
var frag = render_doc(doc);
@@ -406,13 +406,7 @@ Meteor.ui.renderList = function (what, options) {
}
};
- if (what instanceof Collection.LiveResultsSet) {
- var query = what;
- query.reconnect(query_opts);
- } else {
- query_opts.sort = options.sort;
- var query = what.findLive(options.selector || {}, query_opts);
- }
+ var live_results = query.observe(observe_callbacks);
if (!outer_range)
create_outer_range(render_empty());
diff --git a/packages/liveui/liveui_tests.js b/packages/liveui/liveui_tests.js
index c5ff160d45..5873cd5412 100644
--- a/packages/liveui/liveui_tests.js
+++ b/packages/liveui/liveui_tests.js
@@ -349,8 +349,7 @@ test("render - events", function () {
test("renderList - basics", function () {
var c = Meteor.Collection();
- var r = Meteor.ui.renderList(c, {
- sort: ["id"],
+ var r = Meteor.ui.renderList(c.find({}, {sort: ['id']}), {
render: function (doc) {
return DIV({id: doc.id});
},
@@ -428,8 +427,7 @@ test("renderList - removal", function () {
c.update({id: "F"}, {$set: {id: "F2"}});
c.update({id: "C"}, {$set: {id: "C2"}});
- var r = Meteor.ui.renderList(c, {
- sort: ["id"],
+ var r = Meteor.ui.renderList(c.find({}, {sort: ['id']}), {
render: function (doc) {
return DIV({id: doc.id});
},
@@ -479,8 +477,7 @@ test("renderList - removal", function () {
test("renderList - default render empty", function () {
var c = Meteor.Collection();
- var r = Meteor.ui.renderList(c, {
- sort: ["id"],
+ var r = Meteor.ui.renderList(c.find({}, {sort: ['id']}), {
render: function (doc) {
return DIV({id: doc.id});
}
@@ -496,8 +493,7 @@ test("renderList - default render empty", function () {
test("renderList - change and move", function () {
var c = Meteor.Collection();
- var r = Meteor.ui.renderList(c, {
- sort: ["id"],
+ var r = Meteor.ui.renderList(c.find({}, {sort: ['id']}), {
render: function (doc) {
return DIV({id: doc.id});
}
@@ -529,8 +525,7 @@ test("renderList - change and move", function () {
test("renderList - termination", function () {
var c = Meteor.Collection();
- var r = Meteor.ui.renderList(c, {
- sort: ["id"],
+ var r = Meteor.ui.renderList(c.find({}, {sort: ['id']}), {
render: function (doc) {
return DIV({id: doc.id});
}
@@ -557,8 +552,7 @@ test("renderList - termination", function () {
c.remove();
c.insert({id: "A"});
c.insert({id: "B"});
- r = Meteor.ui.renderList(c, {
- sort: ["id"],
+ r = Meteor.ui.renderList(c.find({}, {sort: ['id']}), {
render: function (doc) {
return DIV({id: doc.id});
}
@@ -626,8 +620,7 @@ test("renderList - list items are reactive", function () {
set_weather("there", "cloudy");
Meteor.flush();
var render_count = 0;
- var r = Meteor.ui.renderList(c, {
- sort: ["id"],
+ var r = Meteor.ui.renderList(c.find({}, {sort: ['id']}), {
render: function (doc) {
render_count++;
if (doc.want_weather)
@@ -814,8 +807,7 @@ test("renderList - multiple elements in an item", function () {
function () {
c.remove();
Meteor.flush();
- r = Meteor.ui.renderList(c, {
- sort: ["moved", "index"],
+ r = Meteor.ui.renderList(c.find({}, {sort: ['moved', 'index']}), {
render: function (doc) {
var ret = [];
for (var i = 0; i < lengths[doc.index]; i++)
@@ -853,10 +845,10 @@ test("renderList - #each", function () {
return get_weather(where);
},
data: function () {
- return c.findLive({x: {$lt: 5}}, {sort: ["x"]});
+ return c.find({x: {$lt: 5}}, {sort: ["x"]});
},
data2: function () {
- return c.findLive({x: {$gt: 5}}, {sort: ["x"]});
+ return c.find({x: {$gt: 5}}, {sort: ["x"]});
}
});
diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js
index 8ccc804d58..68c49ce261 100644
--- a/packages/minimongo/minimongo.js
+++ b/packages/minimongo/minimongo.js
@@ -1,37 +1,26 @@
// XXX indexes
// XXX type checking on selectors (graceful error if malformed)
+// XXX merge ad-hoc live query object and Query
+
+// XXX keep observe()s running after they're killed, because usually
+// the dependency tracker is going to spin them up again right away.
+
+// Collection: a set of documents that supports queries and modifiers.
+
+// Query: a specification for a particular subset of documents, w/
+// a defined order, limit, and offset. creating a Query with Collection.find(),
+
+// LiveResultsSet: the return value of a live query.
Collection = function () {
this.docs = {}; // _id -> document (also containing id)
- this.next_qid = 1; // query id generator
+ this.next_qid = 1; // live query id generator
- // qid -> query object. keys: selector_f, sort_f, (callbacks)
+ // qid -> live query object. keys: selector_f, sort_f, (callbacks)
this.queries = {};
};
- // XXX enforce rule that field names can't start with '$' or contain '.'
- // (real mongodb does in fact enforce this)
- // XXX possibly enforce that 'undefined' does not appear (we assume
- // this in our handling of null and $exists)
-Collection.prototype.insert = function (doc) {
- var self = this;
- doc = Collection._deepcopy(doc);
- // XXX deal with mongo's binary id type?
- if (!('_id' in doc))
- doc._id = Collection.uuid();
- // XXX check to see that there is no object with this _id yet?
- self.docs[doc._id] = doc;
- for (var qid in self.queries) {
- var query = self.queries[qid];
- if (query.selector_f(doc))
- Collection._insertInResults(query, doc);
- }
-};
-
-// XXX add one more sort form: "key"
-// and tests, etc
-
// options may include sort, skip, limit, reactive
// sort may be any of these forms:
// {a: 1, b: -1}
@@ -48,63 +37,221 @@ Collection.prototype.insert = function (doc) {
// doc?)
//
// XXX sort does not yet support subkeys ('a.b') .. fix that!
+// XXX add one more sort form: "key"
+// XXX tests
Collection.prototype.find = function (selector, options) {
- var self = this;
- options = options || {};
+ return new Collection.Query(this, selector, options);
+};
- var results = null;
+// don't call this ctor directly. use Collection.find().
+Collection.Query = function (collection, selector, options) {
+ if (!options) options = {};
- if (typeof selector === 'string') {
- // XXX fast path for single object. NOTE: this is actually a
- // different return type than {_id: id} (either object or null, not
- // array). Maybe rename this findOne to match mongo?
- results = self.docs[selector] || null;
- results = Collection._deepcopy(results);
+ this.collection = collection;
+
+ if (typeof selector === "string") {
+ // stash for fast path
+ this.selector_id = selector;
+ this.selector_f = Collection._compileSelector({_id: selector});
} else {
-
- var selector_f = Collection._compileSelector(selector);
- var sort_f = options.sort && Collection._compileSort(options.sort);
- results = self._rawFind(selector_f, sort_f);
-
- if (options.skip)
- results.splice(0, options.skip);
- if (options.limit !== undefined) {
- var limit = options.limit;
- if (results.length > limit)
- results.length = limit;
- }
- for (var i = 0; i < results.length; i++)
- results[i] = Collection._deepcopy(results[i]);
-
+ this.selector_f = Collection._compileSelector(selector);
+ this.sort_f = options.sort ? Collection._compileSort(options.sort) : null;
+ this.skip = options.skip;
+ this.limit = options.limit;
}
- // support Meteor.deps if present
- var reactive = (options.reactive === undefined) ? true : options.reactive;
- var context = reactive && typeof Meteor === "object" && Meteor.deps &&
- Meteor.deps.Context.current;
+ this.db_objects = null;
+ this.cursor_pos = 0;
+
+ // by default, queries register w/ Meteor.deps when it is available.
+ if (typeof Meteor === "object" && Meteor.deps)
+ this.reactive = (options.reactive === undefined) ? true : options.reactive;
+};
+
+// fetch array of documents from the cursor. defaults to all
+// remaining docs, or limited to specified length. returns [] once
+// all documents have been consumed.
+Collection.Query.prototype.fetch = function (length) {
+ var self = this;
+
+ if (self.reactive)
+ self._markAsReactive({added: true,
+ removed: true,
+ changed: true,
+ moved: true});
+
+ if (self.db_objects === null)
+ self.db_objects = self._getRawObjects();
+
+ var idx_end = length ? (length + self.cursor_pos) : undefined;
+ var objects = self.db_objects.slice(self.cursor_pos, idx_end);
+ self.cursor_pos += objects.length;
+
+ var results = [];
+ for (var i = 0; i < objects.length; i++)
+ results.push(Collection._deepcopy(objects[i]));
+ return results;
+};
+
+Collection.Query.prototype.rewind = function () {
+ var self = this;
+ self.db_objects = null;
+ self.cursor_pos = 0;
+};
+
+Collection.Query.prototype.get = function (offset) {
+ var self = this;
+ offset = offset || 0;
+ return self.fetch(offset + 1).pop();
+};
+
+Collection.prototype.findOne = function (selector, options) {
+ return this.find(selector, options).get();
+};
+
+Collection.STOP = 'stop';
+Collection.Query.prototype.forEach = function (callback) {
+ var self = this;
+ var doc;
+
+ while ((doc = self.get()))
+ if (callback(doc) === Collection.STOP)
+ // callback requests we break out of forEach
+ return;
+};
+
+Collection.Query.prototype.map = function (callback) {
+ var self = this;
+ var res = [];
+ var doc;
+
+ while ((doc = self.get()))
+ res.push(callback(doc));
+ return res;
+};
+
+Collection.Query.prototype.count = function () {
+ var self = this;
+
+ if (self.reactive)
+ self._markAsReactive({added: true, removed: true});
+
+ if (self.db_objects === null)
+ self.db_objects = self._getRawObjects();
+
+ return self.db_objects.length;
+};
+
+// the handle that comes back from observe.
+Collection.LiveResultsSet = function () {};
+
+Collection.Query.prototype.observe = function (options) {
+ var self = this;
+
+ if (self.skip || self.limit)
+ throw new Error("cannot observe queries with skip or limit");
+
+ var qid = self.collection.next_qid++;
+
+ // XXX merge this object w/ "this" Query. they're the same.
+ var query = self.collection.queries[qid] = {
+ selector_f: self.selector_f, // not fast pathed
+ sort_f: self.sort_f,
+ results: []
+ };
+ query.results = self._getRawObjects();
+
+ query.added = options.added || function () {};
+ query.changed = options.changed || function () {};
+ query.moved = options.moved || function () {};
+ query.removed = options.removed || function () {};
+ if (!options._suppress_initial)
+ for (var i = 0; i < query.results.length; i++)
+ query.added(Collection._deepcopy(query.results[i]), i);
+
+ var handle = new Collection.LiveResultsSet;
+ _.extend(handle, {
+ stop: function () {
+ delete self.collection.queries[qid];
+ },
+ indexOf: function (id) {
+ for (var i = 0; i < query.results.length; i++)
+ if (query.results[i]._id === id)
+ return i;
+ return -1;
+ },
+ collection: self.collection
+ });
+ return handle;
+};
+
+// constructs sorted array of matching objects, but doesn't copy them.
+// respects sort, skip, and limit properties of the query.
+// if sort_f is falsey, no sort -- you get the natural order
+Collection.Query.prototype._getRawObjects = function () {
+ var self = this;
+
+ // fast path for single ID value
+ if (self.selector_id)
+ return [self.collection.docs[self.selector_id]];
+
+ // slow path for arbitrary selector, sort, skip, limit
+ var results = [];
+ for (var id in self.collection.docs) {
+ var doc = self.collection.docs[id];
+ if (self.selector_f(doc))
+ results.push(doc);
+ }
+
+ if (self.sort_f)
+ results.sort(self.sort_f);
+
+ var idx_start = self.skip || 0;
+ var idx_end = self.limit ? (self.limit + idx_start) : undefined;
+ return results.slice(idx_start, idx_end);
+};
+
+Collection.Query.prototype._markAsReactive = function (options) {
+ var self = this;
+
+ var context = Meteor.deps.Context.current;
+
if (context) {
var invalidate = _.bind(context.invalidate, context);
- var new_options = _.clone(options);
- _.extend(new_options, {
- added: invalidate,
- removed: invalidate,
- changed: invalidate,
- moved: invalidate,
- _suppress_initial: true
- });
+ var handle = self.observe({added: options.added && invalidate,
+ removed: options.removed && invalidate,
+ changed: options.changed && invalidate,
+ moved: options.moved && invalidate,
+ _suppress_initial: true});
- var live_handle = self.findLive(selector, new_options);
- context.on_invalidate(function () {
- // XXX in many cases, the query will be immediately
- // recreated. so we might want to let it linger for a little
- // while and repurpose it if it comes back. this will save us
- // work because we won't have to redo the initial find.
- live_handle.stop();
- });
+ // XXX in many cases, the query will be immediately
+ // recreated. so we might want to let it linger for a little
+ // while and repurpose it if it comes back. this will save us
+ // work because we won't have to redo the initial find.
+ context.on_invalidate(handle.stop);
}
+};
- return results;
+// XXX enforce rule that field names can't start with '$' or contain '.'
+// (real mongodb does in fact enforce this)
+// XXX possibly enforce that 'undefined' does not appear (we assume
+// this in our handling of null and $exists)
+Collection.prototype.insert = function (doc) {
+ var self = this;
+ doc = Collection._deepcopy(doc);
+ // XXX deal with mongo's binary id type?
+ if (!('_id' in doc))
+ doc._id = Collection.uuid();
+ // XXX check to see that there is no object with this _id yet?
+ self.docs[doc._id] = doc;
+
+ // trigger live queries that match
+ for (var qid in self.queries) {
+ var query = self.queries[qid];
+ if (query.selector_f(doc))
+ Collection._insertInResults(query, doc);
+ }
};
// options to contain:
@@ -118,9 +265,6 @@ Collection.prototype.find = function (selector, options) {
// attributes available on returned query handle:
// * stop(): end updates
// * indexOf(id): return current index of object in result set, or -1
-// * reconnect({}): replace added, changed, moved, removed, from the
-// arguments, and call added to deliver the current state of the
-// query (XXX ugly hack to support templating)
// * collection: the collection this query is querying
//
// iff x is a returned query handle, (x instanceof
@@ -134,64 +278,6 @@ Collection.prototype.find = function (selector, options) {
// query, not just its id
// XXX document that initial results will definitely be delivered before we return [do, add to asana]
-Collection.LiveResultsSet = function () {};
-Collection.prototype.findLive = function (selector, options) {
- var self = this;
- var qid = self.next_qid++;
- if (typeof(selector) === "string")
- selector = {_id: selector};
-
- var query = self.queries[qid] = {
- selector_f: Collection._compileSelector(selector),
- sort_f: options.sort ? Collection._compileSort(options.sort) : null,
- results: []
- };
- query.results = self._rawFind(query.selector_f, query.sort_f);
-
- var connect = function (options) {
- query.added = options.added || function () {};
- query.changed = options.changed || function () {};
- query.moved = options.moved || function () {};
- query.removed = options.removed || function () {};
- if (!options._suppress_initial)
- for (var i = 0; i < query.results.length; i++)
- query.added(Collection._deepcopy(query.results[i]), i);
- };
-
- connect(options);
-
- var handle = new Collection.LiveResultsSet;
- _.extend(handle, {
- stop: function () {
- delete self.queries[qid];
- },
- indexOf: function (id) {
- for (var i = 0; i < query.results.length; i++)
- if (query.results[i]._id === id)
- return i;
- return -1;
- },
- reconnect: connect,
- collection: this
- });
- return handle;
-};
-
-// returns matching objects, but doesn't copy them
-// if sort_f is falsey, no sort -- you get the natural order
-Collection.prototype._rawFind = function (selector_f, sort_f) {
- var self = this;
- var results = [];
- for (var id in self.docs) {
- var doc = self.docs[id];
- if (selector_f(doc))
- results.push(doc);
- }
- if (sort_f)
- results.sort(sort_f);
- return results;
-};
-
Collection.prototype.remove = function (selector) {
var self = this;
var remove = [];
@@ -214,7 +300,7 @@ Collection.prototype.remove = function (selector) {
delete self.docs[remove[i]];
}
- // run findLive callbacks _after_ we've removed the documents.
+ // run live query callbacks _after_ we've removed the documents.
for (var i = 0; i < query_remove.length; i++) {
Collection._removeFromResults(query_remove[i][0], query_remove[i][1]);
}
diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js
index a71b6b2fc3..95566e7d46 100644
--- a/packages/minimongo/minimongo_tests.js
+++ b/packages/minimongo/minimongo_tests.js
@@ -45,22 +45,39 @@ test("minimongo - basics", function () {
c.insert({type: "cryptographer", name: "alice"});
c.insert({type: "cryptographer", name: "bob"});
c.insert({type: "cryptographer", name: "cara"});
- assert.length(c.find({type: "kitten"}), 2);
- assert.length(c.find({type: "cryptographer"}), 3);
+ assert.equal(c.find().count(), 5);
+ assert.equal(c.find({type: "kitten"}).count(), 2);
+ assert.equal(c.find({type: "cryptographer"}).count(), 3);
+ assert.length(c.find({type: "kitten"}).fetch(), 2);
+ assert.length(c.find({type: "cryptographer"}).fetch(), 3);
+
c.remove({name: "cara"});
- assert.length(c.find({type: "kitten"}), 2);
- assert.length(c.find({type: "cryptographer"}), 2);
+ assert.equal(c.find().count(), 4);
+ assert.equal(c.find({type: "kitten"}).count(), 2);
+ assert.equal(c.find({type: "cryptographer"}).count(), 2);
+ assert.length(c.find({type: "kitten"}).fetch(), 2);
+ assert.length(c.find({type: "cryptographer"}).fetch(), 2);
+
c.update({name: "snookums"}, {$set: {type: "cryptographer"}});
- assert.length(c.find({type: "kitten"}), 1);
- assert.length(c.find({type: "cryptographer"}), 3);
+ assert.equal(c.find().count(), 4);
+ assert.equal(c.find({type: "kitten"}).count(), 1);
+ assert.equal(c.find({type: "cryptographer"}).count(), 3);
+ assert.length(c.find({type: "kitten"}).fetch(), 1);
+ assert.length(c.find({type: "cryptographer"}).fetch(), 3);
c.remove({});
+ assert.equal(0, c.find().count());
+
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"]});
- assert.length(c.find({tags: "flower"}), 1);
- assert.length(c.find({tags: "fruit"}), 2);
- assert.length(c.find({tags: "red"}), 3);
+
+ assert.equal(c.find({tags: "flower"}).count(), 1);
+ assert.equal(c.find({tags: "fruit"}).count(), 2);
+ assert.equal(c.find({tags: "red"}).count(), 3);
+ assert.length(c.find({tags: "flower"}).fetch(), 1);
+ assert.length(c.find({tags: "fruit"}).fetch(), 2);
+ assert.length(c.find({tags: "red"}).fetch(), 3);
var ev = "";
var makecb = function (tag) {
@@ -74,7 +91,7 @@ test("minimongo - basics", function () {
assert.equal(ev, x);
ev = "";
};
- c.findLive({tags: "flower"}, makecb('a'));
+ c.find({tags: "flower"}).observe(makecb('a'));
expect("aa3_");
c.update({name: "rose"}, {$set: {tags: ["bloom", "red", "squishy"]}});
expect("ra3_");
@@ -88,6 +105,57 @@ test("minimongo - basics", function () {
expect("aa4_");
});
+test("minimongo - cursors", function () {
+ var c = new Collection();
+ var res;
+
+ for (var i = 0; i < 20; i++)
+ c.insert({i: i});
+
+ var q = c.find();
+ assert.equal(q.count(), 20);
+
+ // first 5
+ res = q.fetch(5);
+ assert.length(res, 5);
+ for (var i = 0; i < 5; i++)
+ assert.equal(res[i].i, i);
+ // next 5
+ res = q.fetch(5);
+ assert.length(res, 5);
+ for (var i = 0; i < 5; i++)
+ assert.equal(res[i].i, i + 5);
+ // get
+ res = q.get();
+ assert.equal(res.i, 10);
+ res = q.get(2);
+ assert.equal(res.i, 13);
+ // foreach should consume 3 objects
+ var count = 0;
+ q.forEach(function (obj) {
+ count++;
+ if (count === 3)
+ return Collection.STOP;
+ });
+ res = q.get(0);
+ assert.equal(res.i, 17);
+
+ // rewind
+ q.rewind();
+ assert.equal(q.get().i, 0);
+ // remaining 19 in map
+ res = q.map(function (x) { return x.i; });
+ assert.length(res, 19);
+ for (var i = 0; i < 19; i++)
+ assert.equal(res[i], i + 1);
+
+ // findOne
+ assert.equal(c.findOne({i: 0}).i, 0);
+ assert.equal(c.findOne({i: 1}).i, 1);
+ var id = c.findOne({i: 2})._id;
+ assert.equal(c.findOne(id).i, 2);
+});
+
test("minimongo - misc", function () {
// deepcopy
var a = {a: [1, 2, 3], b: "x", c: true, d: {x: 12, y: [12]},
@@ -457,7 +525,7 @@ test("minimongo - sort", function () {
c.insert({a: i, b: j, _id: i + "_" + j});
assert.equal(
- c.find({a: {$gt: 10}}, {sort: {b: -1, a: 1}, limit: 5}), [
+ c.find({a: {$gt: 10}}, {sort: {b: -1, a: 1}, limit: 5}).fetch(), [
{a: 11, b: 1, _id: "11_1"},
{a: 12, b: 1, _id: "12_1"},
{a: 13, b: 1, _id: "13_1"},
@@ -465,7 +533,7 @@ test("minimongo - sort", function () {
{a: 15, b: 1, _id: "15_1"}]);
assert.equal(
- c.find({a: {$gt: 10}}, {sort: {b: -1, a: 1}, skip: 3, limit: 5}), [
+ c.find({a: {$gt: 10}}, {sort: {b: -1, a: 1}, skip: 3, limit: 5}).fetch(), [
{a: 14, b: 1, _id: "14_1"},
{a: 15, b: 1, _id: "15_1"},
{a: 16, b: 1, _id: "16_1"},
@@ -473,7 +541,7 @@ test("minimongo - sort", function () {
{a: 18, b: 1, _id: "18_1"}]);
assert.equal(
- c.find({a: {$gte: 20}}, {sort: {a: 1, b: -1}, skip: 50, limit: 5}), [
+ c.find({a: {$gte: 20}}, {sort: {a: 1, b: -1}, skip: 50, limit: 5}).fetch(), [
{a: 45, b: 1, _id: "45_1"},
{a: 45, b: 0, _id: "45_0"},
{a: 46, b: 1, _id: "46_1"},
diff --git a/packages/templating/deftemplate.js b/packages/templating/deftemplate.js
index eaab440f31..8e4b40414a 100644
--- a/packages/templating/deftemplate.js
+++ b/packages/templating/deftemplate.js
@@ -19,13 +19,13 @@ Meteor._pending_partials_idx_nonce = 0;
// XXX another messy hack -- we reach into handlebars and extend #each
// to know how to cooperate with pending_partials and minimongo
-// findlive.
+// observe.
Meteor._hook_handlebars_each = function () {
Meteor._hook_handlebars_each = function(){}; // install the hook only once
var orig = Handlebars._default_helpers.each;
Handlebars._default_helpers.each = function (context, options) {
- if (!(context instanceof Collection.LiveResultsSet))
+ if (!(context instanceof Collection.Query))
return orig(context, options);
var id = Meteor._pending_partials_idx_nonce++;