mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
New Collection API
The new Collection API separates query handles from result sets. It
allows template iterators to only redraw changed objects instead of
entire result sets. This implementation also sets the stage for
minimongo indexes and better invalidation performance.
collection.find() now returns a Collection.Query handle. To retrieve
results we provide these methods on Collection.Query:
Iterators (encouraged way to access results):
* query.forEach(function (obj) { ... });
* results = query.map(function (obj) { ... });
Cursor-based retrieval (iterators are built on fetch):
* docs = query.fetch(maxlen); // return next [maxlen] (all) docs.
* doc = query.get(skip); // return next doc, skipping [skip] (0) docs.
Counter:
* length = query.count(); // number of results in query.
Live queries (replaces findLive):
* live_handle = query.observe({added: function (obj, idx) { ... },
removed: function (id, idx) { ... },
changed: function (obj, idx) { ... },
moved: function (obj, old_idx, new_idx) { ... }});
Convenience finders:
* doc = collection.findOne({color: 'red'});
* doc = collection.findOne(id_val);
On the client, calling forEach(), map(), fetch(), get(), or findOne()
inside an invalidation context will register a dependency on the entire
query result. Any change to any objects invalidates the context.
Calling count() inside an invalidation context will register a
dependency that only triggers if objects enter or leave the result set.
Calling observe() does not register a dependency.
This commit is contained in:
committed by
Nick Martin
parent
a028b9d9de
commit
5e7daaaff3
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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 $('<div class="none">Click a player to select</div>');
|
||||
|
||||
var player = Players.find(selected_player);
|
||||
var player = Players.findOne(selected_player);
|
||||
return $('<div class="details"><div class="name">' + player.name +
|
||||
'</div><input type="button" value="Give 5 points"></div>');
|
||||
}, {
|
||||
@@ -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",
|
||||
|
||||
@@ -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} });
|
||||
|
||||
2
examples/todos/server/bootstrap.js
vendored
2
examples/todos/server/bootstrap.js
vendored
@@ -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: [
|
||||
|
||||
@@ -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}});
|
||||
}};
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
@@ -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);
|
||||
|
||||
})();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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"]});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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++;
|
||||
|
||||
Reference in New Issue
Block a user