mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge remote-tracking branch 'origin/shark' into shark-newargs
Conflicts: packages/spacebars-compiler/compile_tests.js packages/spacebars-compiler/spacebars-compiler.js
This commit is contained in:
@@ -20,6 +20,15 @@
|
||||
* Add `frame-src` to `browser-policy-content` and account for
|
||||
cross-browser CSP disparities.
|
||||
|
||||
* Transform functions must return objects and may not change the `_id` field
|
||||
(though they may leave it out)
|
||||
|
||||
* Upgrade jQuery from 1.8.2 to 1.10.2.
|
||||
XXX see http://jquery.com/upgrade-guide/1.9/ for incompatibilities
|
||||
XXX consider taking 1.11 instead, which was released this week
|
||||
|
||||
* `force-ssl`: don't require SSL during `meteor run` in IPv6 environments.
|
||||
|
||||
|
||||
## v0.7.0.1
|
||||
|
||||
|
||||
@@ -654,7 +654,8 @@ methods, documents are passed through the `transform` function before being
|
||||
returned or passed to callbacks. This allows you to add methods or otherwise
|
||||
modify the contents of your collection from their database representation. You
|
||||
can also specify `transform` on a particular `find`, `findOne`, `allow`, or
|
||||
`deny` call.
|
||||
`deny` call. Transform functions must return an object and they may not change
|
||||
the value of the document's `_id` field (though it's OK to leave it out).
|
||||
|
||||
// An Animal class that takes a document in its constructor
|
||||
Animal = function (doc) {
|
||||
|
||||
4
meteor
4
meteor
@@ -1,6 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
BUNDLE_VERSION=0.3.28
|
||||
# danger will robinson! mother:config/download-dev-bundles.sh only goes up to
|
||||
# 0.3.30!
|
||||
BUNDLE_VERSION=0.3.29
|
||||
|
||||
# OS Check. Put here because here is where we download the precompiled
|
||||
# bundles that are arch specific.
|
||||
|
||||
@@ -22,11 +22,12 @@ httpServer.addListener('request', function (req, res) {
|
||||
// Determine if the connection is only over localhost. Both we
|
||||
// received it on localhost, and all proxies involved received on
|
||||
// localhost.
|
||||
var localhostRegexp = /^\s*(127\.0\.0\.1|::1)\s*$/;
|
||||
var isLocal = (
|
||||
remoteAddress === "127.0.0.1" &&
|
||||
localhostRegexp.test(remoteAddress) &&
|
||||
(!req.headers['x-forwarded-for'] ||
|
||||
_.all(req.headers['x-forwarded-for'].split(','), function (x) {
|
||||
return /\s*127\.0\.0\.1\s*/.test(x);
|
||||
return localhostRegexp.test(x);
|
||||
})));
|
||||
|
||||
// Determine if the connection was over SSL at any point. Either we
|
||||
|
||||
@@ -6,71 +6,3 @@
|
||||
Meteor._noYieldsAllowed = function (f) {
|
||||
return f();
|
||||
};
|
||||
|
||||
// An even simpler queue of tasks than the fiber-enabled one. This one just
|
||||
// runs all the tasks when you call runTask or flush, synchronously.
|
||||
//
|
||||
Meteor._SynchronousQueue = function () {
|
||||
var self = this;
|
||||
self._tasks = [];
|
||||
self._running = false;
|
||||
};
|
||||
|
||||
_.extend(Meteor._SynchronousQueue.prototype, {
|
||||
runTask: function (task) {
|
||||
var self = this;
|
||||
if (!self.safeToRunTask())
|
||||
throw new Error("Could not synchronously run a task from a running task");
|
||||
self._tasks.push(task);
|
||||
var tasks = self._tasks;
|
||||
self._tasks = [];
|
||||
self._running = true;
|
||||
try {
|
||||
while (!_.isEmpty(tasks)) {
|
||||
var t = tasks.shift();
|
||||
try {
|
||||
t();
|
||||
} catch (e) {
|
||||
if (_.isEmpty(tasks)) {
|
||||
// this was the last task, that is, the one we're calling runTask
|
||||
// for.
|
||||
throw e;
|
||||
} else {
|
||||
Meteor._debug("Exception in queued task: " + e.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
self._running = false;
|
||||
}
|
||||
},
|
||||
|
||||
queueTask: function (task) {
|
||||
var self = this;
|
||||
var wasEmpty = _.isEmpty(self._tasks);
|
||||
self._tasks.push(task);
|
||||
// Intentionally not using Meteor.setTimeout, because it doesn't like runing
|
||||
// in stubs for now.
|
||||
if (wasEmpty)
|
||||
setTimeout(_.bind(self.flush, self), 0);
|
||||
},
|
||||
|
||||
flush: function () {
|
||||
var self = this;
|
||||
self.runTask(function () {});
|
||||
},
|
||||
|
||||
drain: function () {
|
||||
var self = this;
|
||||
if (!self.safeToRunTask())
|
||||
return;
|
||||
while (!_.isEmpty(self._tasks)) {
|
||||
self.flush();
|
||||
}
|
||||
},
|
||||
|
||||
safeToRunTask: function () {
|
||||
var self = this;
|
||||
return !self._running;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ Package.on_use(function (api) {
|
||||
api.add_files('errors.js', ['client', 'server']);
|
||||
api.add_files('fiber_helpers.js', 'server');
|
||||
api.add_files('fiber_stubs_client.js', 'client');
|
||||
api.add_files('unyielding_queue.js');
|
||||
api.add_files('startup_client.js', ['client']);
|
||||
api.add_files('startup_server.js', ['server']);
|
||||
api.add_files('debug.js', ['client', 'server']);
|
||||
|
||||
72
packages/meteor/unyielding_queue.js
Normal file
72
packages/meteor/unyielding_queue.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// A simpler version of Meteor._SynchronousQueue with the same external
|
||||
// interface. It runs on both client and server, unlike _SynchronousQueue which
|
||||
// only runs on the server. When used on the server, tasks may not yield. This
|
||||
// one just runs all the tasks when you call runTask or flush, synchronously.
|
||||
// It itself also does not yield.
|
||||
//
|
||||
Meteor._UnyieldingQueue = function () {
|
||||
var self = this;
|
||||
self._tasks = [];
|
||||
self._running = false;
|
||||
};
|
||||
|
||||
_.extend(Meteor._UnyieldingQueue.prototype, {
|
||||
runTask: function (task) {
|
||||
var self = this;
|
||||
if (!self.safeToRunTask())
|
||||
throw new Error("Could not synchronously run a task from a running task");
|
||||
self._tasks.push(task);
|
||||
var tasks = self._tasks;
|
||||
self._tasks = [];
|
||||
self._running = true;
|
||||
try {
|
||||
while (!_.isEmpty(tasks)) {
|
||||
var t = tasks.shift();
|
||||
try {
|
||||
Meteor._noYieldsAllowed(function () {
|
||||
t();
|
||||
});
|
||||
} catch (e) {
|
||||
if (_.isEmpty(tasks)) {
|
||||
// this was the last task, that is, the one we're calling runTask
|
||||
// for.
|
||||
throw e;
|
||||
} else {
|
||||
Meteor._debug("Exception in queued task: " + e.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
self._running = false;
|
||||
}
|
||||
},
|
||||
|
||||
queueTask: function (task) {
|
||||
var self = this;
|
||||
var wasEmpty = _.isEmpty(self._tasks);
|
||||
self._tasks.push(task);
|
||||
// Intentionally not using Meteor.setTimeout, because it doesn't like runing
|
||||
// in stubs for now.
|
||||
if (wasEmpty)
|
||||
setTimeout(_.bind(self.flush, self), 0);
|
||||
},
|
||||
|
||||
flush: function () {
|
||||
var self = this;
|
||||
self.runTask(function () {});
|
||||
},
|
||||
|
||||
drain: function () {
|
||||
var self = this;
|
||||
if (!self.safeToRunTask())
|
||||
return;
|
||||
while (!_.isEmpty(self._tasks)) {
|
||||
self.flush();
|
||||
}
|
||||
},
|
||||
|
||||
safeToRunTask: function () {
|
||||
var self = this;
|
||||
return !self._running;
|
||||
}
|
||||
});
|
||||
@@ -7,13 +7,34 @@
|
||||
|
||||
// ObserveHandle: the return value of a live query.
|
||||
|
||||
LocalCollection = function (name) {
|
||||
LocalCollection = function (options) {
|
||||
var self = this;
|
||||
self.name = name;
|
||||
options = options || {};
|
||||
|
||||
self.name = options.name;
|
||||
// _id -> document (also containing id)
|
||||
self._docs = new LocalCollection._IdMap;
|
||||
|
||||
self._observeQueue = new Meteor._SynchronousQueue();
|
||||
// When writing to this collection, we batch all observeChanges callbacks
|
||||
// until the end of the write, and run them at this point. On the server, we
|
||||
// use a single SynchronousQueue to do so, so that we never deliver callbacks
|
||||
// out of order even if other writes occur during a yield. On the client, or
|
||||
// on the server if we promise that our callbacks will never yield via an
|
||||
// undocumented option, we use the simpler UnyieldingQueue.
|
||||
//
|
||||
// (What is the _observeCallbacksWillNeverYield option for? In some cases, it
|
||||
// can be nice (on the server) to be able to write to a LocalCollection
|
||||
// without yielding (eg, in a _noYieldsAllowed block). It's necessary to
|
||||
// provide non-yielding allow callbacks in that case, but just doing that
|
||||
// wouldn't be good enough if we always used SynchronousQueue on the server,
|
||||
// since it tends to yield in order to run even non-yielding callbacks.)
|
||||
var queueClass;
|
||||
if (Meteor._SynchronousQueue && !options._observeCallbacksWillNeverYield) {
|
||||
queueClass = Meteor._SynchronousQueue;
|
||||
} else {
|
||||
queueClass = Meteor._UnyieldingQueue;
|
||||
}
|
||||
self._observeQueue = new queueClass();
|
||||
|
||||
self.next_qid = 1; // live query id generator
|
||||
|
||||
@@ -21,13 +42,13 @@ LocalCollection = function (name) {
|
||||
// ordered: bool. ordered queries have addedBefore/movedBefore callbacks.
|
||||
// results: array (ordered) or object (unordered) of current results
|
||||
// (aliased with self._docs!)
|
||||
// results_snapshot: snapshot of results. null if not paused.
|
||||
// resultsSnapshot: snapshot of results. null if not paused.
|
||||
// cursor: Cursor object for the query.
|
||||
// selector, sorter, (callbacks): functions
|
||||
self.queries = {};
|
||||
|
||||
// null if not saving originals; an IdMap from id to original document value if
|
||||
// saving originals. See comments before saveOriginals().
|
||||
// null if not saving originals; an IdMap from id to original document value
|
||||
// if saving originals. See comments before saveOriginals().
|
||||
self._savedOriginals = null;
|
||||
|
||||
// True when observers are paused and we should not send callbacks.
|
||||
@@ -107,12 +128,9 @@ LocalCollection.Cursor = function (collection, selector, options) {
|
||||
self.fields = options.fields;
|
||||
|
||||
if (self.fields)
|
||||
self.projection_f = LocalCollection._compileProjection(self.fields);
|
||||
self.projectionFn = LocalCollection._compileProjection(self.fields);
|
||||
|
||||
if (options.transform && typeof Deps !== "undefined")
|
||||
self._transform = Deps._makeNonreactive(LocalCollection.wrapTransform(options.transform));
|
||||
else
|
||||
self._transform = LocalCollection.wrapTransform(options.transform);
|
||||
self._transform = LocalCollection.wrapTransform(options.transform);
|
||||
|
||||
// db_objects is an array of the objects that match the cursor. (It's always
|
||||
// an array, never an IdMap: LocalCollection.Cursor is always ordered.)
|
||||
@@ -152,7 +170,7 @@ LocalCollection.Cursor.prototype.forEach = function (callback, thisArg) {
|
||||
var self = this;
|
||||
|
||||
if (self.db_objects === null)
|
||||
self.db_objects = self._getRawObjects(true);
|
||||
self.db_objects = self._getRawObjects({ordered: true});
|
||||
|
||||
if (self.reactive)
|
||||
self._depend({
|
||||
@@ -163,8 +181,8 @@ LocalCollection.Cursor.prototype.forEach = function (callback, thisArg) {
|
||||
|
||||
while (self.cursor_pos < self.db_objects.length) {
|
||||
var elt = EJSON.clone(self.db_objects[self.cursor_pos]);
|
||||
if (self.projection_f)
|
||||
elt = self.projection_f(elt);
|
||||
if (self.projectionFn)
|
||||
elt = self.projectionFn(elt);
|
||||
if (self._transform)
|
||||
elt = self._transform(elt);
|
||||
callback.call(thisArg, elt, self.cursor_pos, self);
|
||||
@@ -202,7 +220,7 @@ LocalCollection.Cursor.prototype.count = function () {
|
||||
true /* allow the observe to be unordered */);
|
||||
|
||||
if (self.db_objects === null)
|
||||
self.db_objects = self._getRawObjects(true);
|
||||
self.db_objects = self._getRawObjects({ordered: true});
|
||||
|
||||
return self.db_objects.length;
|
||||
};
|
||||
@@ -284,12 +302,10 @@ _.extend(LocalCollection.Cursor.prototype, {
|
||||
sorter: ordered && self.sorter,
|
||||
distances: (
|
||||
self.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap),
|
||||
results_snapshot: null,
|
||||
resultsSnapshot: null,
|
||||
ordered: ordered,
|
||||
cursor: self,
|
||||
observeChanges: options.observeChanges,
|
||||
fields: self.fields,
|
||||
projection_f: self.projection_f
|
||||
projectionFn: self.projectionFn
|
||||
};
|
||||
var qid;
|
||||
|
||||
@@ -299,9 +315,10 @@ _.extend(LocalCollection.Cursor.prototype, {
|
||||
qid = self.collection.next_qid++;
|
||||
self.collection.queries[qid] = query;
|
||||
}
|
||||
query.results = self._getRawObjects(ordered, query.distances);
|
||||
query.results = self._getRawObjects({
|
||||
ordered: ordered, distances: query.distances});
|
||||
if (self.collection.paused)
|
||||
query.results_snapshot = (ordered ? [] : new LocalCollection._IdMap);
|
||||
query.resultsSnapshot = (ordered ? [] : new LocalCollection._IdMap);
|
||||
|
||||
// wrap callbacks we were passed. callbacks only fire when not paused and
|
||||
// are never undefined
|
||||
@@ -317,17 +334,18 @@ _.extend(LocalCollection.Cursor.prototype, {
|
||||
var context = this;
|
||||
var args = arguments;
|
||||
|
||||
if (fieldsIndex !== undefined && self.projection_f) {
|
||||
args[fieldsIndex] = self.projection_f(args[fieldsIndex]);
|
||||
if (self.collection.paused)
|
||||
return;
|
||||
|
||||
if (fieldsIndex !== undefined && self.projectionFn) {
|
||||
args[fieldsIndex] = self.projectionFn(args[fieldsIndex]);
|
||||
if (ignoreEmptyFields && _.isEmpty(args[fieldsIndex]))
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.collection.paused) {
|
||||
self.collection._observeQueue.queueTask(function () {
|
||||
f.apply(context, args);
|
||||
});
|
||||
}
|
||||
self.collection._observeQueue.queueTask(function () {
|
||||
f.apply(context, args);
|
||||
});
|
||||
};
|
||||
};
|
||||
query.added = wrapCallback(options.added, 1);
|
||||
@@ -395,13 +413,13 @@ _.extend(LocalCollection.Cursor.prototype, {
|
||||
// argument, this function will clear it and use it for this purpose (otherwise
|
||||
// it will just create its own _IdMap). The observeChanges implementation uses
|
||||
// this to remember the distances after this function returns.
|
||||
LocalCollection.Cursor.prototype._getRawObjects = function (ordered,
|
||||
distances) {
|
||||
LocalCollection.Cursor.prototype._getRawObjects = function (options) {
|
||||
var self = this;
|
||||
options = options || {};
|
||||
|
||||
// XXX use OrderedDict instead of array, and make IdMap and OrderedDict
|
||||
// compatible
|
||||
var results = ordered ? [] : new LocalCollection._IdMap;
|
||||
var results = options.ordered ? [] : new LocalCollection._IdMap;
|
||||
|
||||
// fast path for single ID value
|
||||
if (self._selectorId !== undefined) {
|
||||
@@ -413,7 +431,7 @@ LocalCollection.Cursor.prototype._getRawObjects = function (ordered,
|
||||
|
||||
var selectedDoc = self.collection._docs.get(self._selectorId);
|
||||
if (selectedDoc) {
|
||||
if (ordered)
|
||||
if (options.ordered)
|
||||
results.push(selectedDoc);
|
||||
else
|
||||
results.set(self._selectorId, selectedDoc);
|
||||
@@ -426,17 +444,20 @@ LocalCollection.Cursor.prototype._getRawObjects = function (ordered,
|
||||
// in the observeChanges case, distances is actually part of the "query" (ie,
|
||||
// live results set) object. in other cases, distances is only used inside
|
||||
// this function.
|
||||
if (self.matcher.hasGeoQuery() && ordered) {
|
||||
if (distances)
|
||||
var distances;
|
||||
if (self.matcher.hasGeoQuery() && options.ordered) {
|
||||
if (options.distances) {
|
||||
distances = options.distances;
|
||||
distances.clear();
|
||||
else
|
||||
} else {
|
||||
distances = new LocalCollection._IdMap();
|
||||
}
|
||||
}
|
||||
|
||||
self.collection._docs.forEach(function (doc, id) {
|
||||
var matchResult = self.matcher.documentMatches(doc);
|
||||
if (matchResult.result) {
|
||||
if (ordered) {
|
||||
if (options.ordered) {
|
||||
results.push(doc);
|
||||
if (distances && matchResult.distance !== undefined)
|
||||
distances.set(id, matchResult.distance);
|
||||
@@ -452,7 +473,7 @@ LocalCollection.Cursor.prototype._getRawObjects = function (ordered,
|
||||
return true; // continue
|
||||
});
|
||||
|
||||
if (!ordered)
|
||||
if (!options.ordered)
|
||||
return results;
|
||||
|
||||
if (self.sorter) {
|
||||
@@ -542,31 +563,60 @@ LocalCollection.prototype.insert = function (doc, callback) {
|
||||
return id;
|
||||
};
|
||||
|
||||
LocalCollection.prototype.remove = function (selector, callback) {
|
||||
// Iterates over a subset of documents that could match selector; calls
|
||||
// f(doc, id) on each of them. Specifically, if selector specifies
|
||||
// specific _id's, it only looks at those. doc is *not* cloned: it is the
|
||||
// same object that is in _docs.
|
||||
LocalCollection.prototype._eachPossiblyMatchingDoc = function (selector, f) {
|
||||
var self = this;
|
||||
var remove = [];
|
||||
|
||||
var queriesToRecompute = [];
|
||||
var matcher = new Minimongo.Matcher(selector, self);
|
||||
|
||||
// Avoid O(n) for "remove a single doc by ID".
|
||||
var specificIds = LocalCollection._idsMatchedBySelector(selector);
|
||||
if (specificIds) {
|
||||
_.each(specificIds, function (id) {
|
||||
// We still have to run matcher, in case it's something like
|
||||
// {_id: "X", a: 42}
|
||||
for (var i = 0; i < specificIds.length; ++i) {
|
||||
var id = specificIds[i];
|
||||
var doc = self._docs.get(id);
|
||||
if (doc && matcher.documentMatches(doc).result)
|
||||
remove.push(id);
|
||||
});
|
||||
if (doc) {
|
||||
var breakIfFalse = f(doc, id);
|
||||
if (breakIfFalse === false)
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self._docs.forEach(function (doc, id) {
|
||||
if (matcher.documentMatches(doc).result) {
|
||||
remove.push(id);
|
||||
self._docs.forEach(f);
|
||||
}
|
||||
};
|
||||
|
||||
LocalCollection.prototype.remove = function (selector, callback) {
|
||||
var self = this;
|
||||
|
||||
// Easy special case: if we're not calling observeChanges callbacks and we're
|
||||
// not saving originals and we got asked to remove everything, then just empty
|
||||
// everything directly.
|
||||
if (self.paused && !self._savedOriginals && EJSON.equals(selector, {})) {
|
||||
var result = self._docs.size();
|
||||
self._docs.clear();
|
||||
_.each(self.queries, function (query) {
|
||||
if (query.ordered) {
|
||||
query.results = [];
|
||||
} else {
|
||||
query.results.clear();
|
||||
}
|
||||
});
|
||||
if (callback) {
|
||||
Meteor.defer(function () {
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
var matcher = new Minimongo.Matcher(selector, self);
|
||||
var remove = [];
|
||||
self._eachPossiblyMatchingDoc(selector, function (doc, id) {
|
||||
if (matcher.documentMatches(doc).result)
|
||||
remove.push(id);
|
||||
});
|
||||
|
||||
var queriesToRecompute = [];
|
||||
var queryRemove = [];
|
||||
for (var i = 0; i < remove.length; i++) {
|
||||
var removeId = remove[i];
|
||||
@@ -597,7 +647,7 @@ LocalCollection.prototype.remove = function (selector, callback) {
|
||||
LocalCollection._recomputeResults(query);
|
||||
});
|
||||
self._observeQueue.drain();
|
||||
var result = remove.length;
|
||||
result = remove.length;
|
||||
if (callback)
|
||||
Meteor.defer(function () {
|
||||
callback(null, result);
|
||||
@@ -620,7 +670,7 @@ LocalCollection.prototype.update = function (selector, mod, options, callback) {
|
||||
// Save the original results of any query that we might need to
|
||||
// _recomputeResults on, because _modifyAndNotify will mutate the objects in
|
||||
// it. (We don't need to save the original results of paused queries because
|
||||
// they already have a results_snapshot and we won't be diffing in
|
||||
// they already have a resultsSnapshot and we won't be diffing in
|
||||
// _recomputeResults.)
|
||||
var qidToOriginalResults = {};
|
||||
_.each(self.queries, function (query, qid) {
|
||||
@@ -633,7 +683,7 @@ LocalCollection.prototype.update = function (selector, mod, options, callback) {
|
||||
|
||||
var updateCount = 0;
|
||||
|
||||
self._docs.forEach(function (doc, id) {
|
||||
self._eachPossiblyMatchingDoc(selector, function (doc, id) {
|
||||
var queryResult = matcher.documentMatches(doc);
|
||||
if (queryResult.result) {
|
||||
// XXX Should we save the original even if mod ends up being a no-op?
|
||||
@@ -842,7 +892,8 @@ LocalCollection._recomputeResults = function (query, oldResults) {
|
||||
oldResults = query.results;
|
||||
if (query.distances)
|
||||
query.distances.clear();
|
||||
query.results = query.cursor._getRawObjects(query.ordered, query.distances);
|
||||
query.results = query.cursor._getRawObjects({
|
||||
ordered: query.ordered, distances: query.distances});
|
||||
|
||||
if (!query.paused) {
|
||||
LocalCollection._diffQueryChanges(
|
||||
@@ -938,7 +989,7 @@ LocalCollection.prototype.pauseObservers = function () {
|
||||
for (var qid in this.queries) {
|
||||
var query = this.queries[qid];
|
||||
|
||||
query.results_snapshot = EJSON.clone(query.results);
|
||||
query.resultsSnapshot = EJSON.clone(query.results);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -961,8 +1012,8 @@ LocalCollection.prototype.resumeObservers = function () {
|
||||
// Diff the current results against the snapshot and send to observers.
|
||||
// pass the query object for its observer callbacks.
|
||||
LocalCollection._diffQueryChanges(
|
||||
query.ordered, query.results_snapshot, query.results, query);
|
||||
query.results_snapshot = null;
|
||||
query.ordered, query.resultsSnapshot, query.results, query);
|
||||
query.resultsSnapshot = null;
|
||||
}
|
||||
self._observeQueue.drain();
|
||||
};
|
||||
|
||||
@@ -2170,7 +2170,8 @@ Tinytest.add("minimongo - observe ordered", function (test) {
|
||||
handle.stop();
|
||||
|
||||
// test _suppress_initial
|
||||
handle = c.find({}, {sort: {a: -1}}).observe(_.extend(cbs, {_suppress_initial: true}));
|
||||
handle = c.find({}, {sort: {a: -1}}).observe(_.extend({
|
||||
_suppress_initial: true}, cbs));
|
||||
test.equal(operations.shift(), undefined);
|
||||
c.insert({a:100});
|
||||
test.equal(operations.shift(), ['added', {a:100}, 0, idA2]);
|
||||
@@ -2197,6 +2198,21 @@ Tinytest.add("minimongo - observe ordered", function (test) {
|
||||
test.equal(operations.shift(), ['changed', {a:3.5}, 0, {a:3}]);
|
||||
handle.stop();
|
||||
|
||||
// test observe limit with pre-existing docs
|
||||
c.remove({});
|
||||
c.insert({a: 1});
|
||||
c.insert({_id: 'two', a: 2});
|
||||
c.insert({a: 3});
|
||||
handle = c.find({}, {sort: {a: 1}, limit: 2}).observe(cbs);
|
||||
test.equal(operations.shift(), ['added', {a:1}, 0, null]);
|
||||
test.equal(operations.shift(), ['added', {a:2}, 1, null]);
|
||||
test.equal(operations.shift(), undefined);
|
||||
c.remove({a: 2});
|
||||
test.equal(operations.shift(), ['removed', 'two', 1, {a:2}]);
|
||||
test.equal(operations.shift(), ['added', {a:3}, 1, null]);
|
||||
test.equal(operations.shift(), undefined);
|
||||
handle.stop();
|
||||
|
||||
// test _no_indices
|
||||
|
||||
c.remove({});
|
||||
@@ -2565,6 +2581,14 @@ Tinytest.add("minimongo - pause", function (test) {
|
||||
test.equal(operations.shift(), ['changed', {a:3}, 0, {a:1}]);
|
||||
test.length(operations, 0);
|
||||
|
||||
// test special case for remove({})
|
||||
c.pauseObservers();
|
||||
test.equal(c.remove({}), 1);
|
||||
test.length(operations, 0);
|
||||
c.resumeObservers();
|
||||
test.equal(operations.shift(), ['removed', 1, 0, {a:3}]);
|
||||
test.length(operations, 0);
|
||||
|
||||
h.stop();
|
||||
});
|
||||
|
||||
|
||||
@@ -193,11 +193,11 @@ LocalCollection._observeFromObserveChanges = function (cursor, observeCallbacks)
|
||||
//
|
||||
// NOTE: If called from an observe callback for a certain change, the result
|
||||
// is *not* guaranteed to be a snapshot of the cursor up to that
|
||||
// change. This is because callbacks are deferred.
|
||||
// change. This is because the callbacks are invoked before updating docs.
|
||||
handle._fetch = function () {
|
||||
var docsArray = [];
|
||||
changeObserver.docs.forEach(function (doc) {
|
||||
docsArray.push(transform(doc));
|
||||
docsArray.push(transform(EJSON.clone(doc)));
|
||||
});
|
||||
return docsArray;
|
||||
};
|
||||
|
||||
@@ -38,5 +38,6 @@ Package.on_test(function (api) {
|
||||
api.use(['tinytest', 'underscore', 'ejson', 'ordered-dict',
|
||||
'random', 'deps']);
|
||||
api.add_files('minimongo_tests.js', 'client');
|
||||
api.add_files('wrap_transform_tests.js');
|
||||
api.add_files('minimongo_server_tests.js', 'server');
|
||||
});
|
||||
|
||||
@@ -48,11 +48,19 @@ Sorter = function (spec) {
|
||||
// min/max.)
|
||||
//
|
||||
// XXX This is actually wrong! In fact, the whole attempt to compile sort
|
||||
// functions independently of selectors is wrong. In MongoDB, if you have
|
||||
// documents {_id: 'x', a: [1, 10]} and {_id: 'y', a: [5, 15]},
|
||||
// then C.find({}, {sort: {a: 1}}) puts x before y (1 comes before 5).
|
||||
// But C.find({a: {$gt: 3}}, {sort: {a: 1}}) puts y before x (1 does not match
|
||||
// the selector, and 5 comes before 10).
|
||||
// functions independently of selectors is wrong. In MongoDB, if you have
|
||||
// documents {_id: 'x', a: [1, 10]} and {_id: 'y', a: [5, 15]}, then
|
||||
// C.find({}, {sort: {a: 1}}) puts x before y (1 comes before 5). But
|
||||
// C.find({a: {$gt: 3}}, {sort: {a: 1}}) puts y before x (1 does not match
|
||||
// the selector, and 5 comes before 10).
|
||||
//
|
||||
// The way this works is pretty subtle! For example, if the documents are
|
||||
// instead {_id: 'x', a: [{x: 1}, {x: 10}]}) and
|
||||
// {_id: 'y', a: [{x: 5}, {x: 15}]}),
|
||||
// then C.find({'a.x': {$gt: 3}}, {sort: {'a.x': 1}}) and
|
||||
// C.find({a: {$elemMatch: {x: {$gt: 3}}}}, {sort: {'a.x': 1}})
|
||||
// both follow this rule (y before x). ie, you do have to apply this
|
||||
// through $elemMatch.
|
||||
var reduceValue = function (branchValues, findMin) {
|
||||
// Expand any leaf arrays that we find, and ignore those arrays themselves.
|
||||
branchValues = expandArraysInBranches(branchValues, true);
|
||||
|
||||
@@ -7,25 +7,29 @@
|
||||
// - If the return value has an _id field, verify that it matches the
|
||||
// original _id field
|
||||
// - If the return value doesn't have an _id field, add it back.
|
||||
LocalCollection.wrapTransform = function(transform) {
|
||||
LocalCollection.wrapTransform = function (transform) {
|
||||
if (!transform)
|
||||
return undefined;
|
||||
return null;
|
||||
|
||||
return function (doc) {
|
||||
var id = doc._id;
|
||||
var transformed = transform(doc);
|
||||
if (!_.has(doc, '_id')) {
|
||||
// XXX do we ever have a transform on the oplog's collection? because that
|
||||
// collection has no _id.
|
||||
throw new Error("can only transform documents with _id");
|
||||
}
|
||||
|
||||
if (typeof transformed !== 'object' ||
|
||||
transformed instanceof Array ||
|
||||
// Even though fine technically, don't let Mongo ObjectIDs
|
||||
// through. It would suck to think your app works until
|
||||
// you insert the first document using Meteor.
|
||||
transformed instanceof Meteor.Collection.ObjectID) {
|
||||
var id = doc._id;
|
||||
// XXX consider making deps a weak dependency and checking Package.deps here
|
||||
var transformed = Deps.nonreactive(function () {
|
||||
return transform(doc);
|
||||
});
|
||||
|
||||
if (!isPlainObject(transformed)) {
|
||||
throw new Error("transform must return object");
|
||||
}
|
||||
|
||||
if (transformed._id) {
|
||||
if (transformed._id !== id) {
|
||||
if (_.has(transformed, '_id')) {
|
||||
if (!EJSON.equals(transformed._id, id)) {
|
||||
throw new Error("transformed document can't have different _id");
|
||||
}
|
||||
} else {
|
||||
|
||||
58
packages/minimongo/wrap_transform_tests.js
Normal file
58
packages/minimongo/wrap_transform_tests.js
Normal file
@@ -0,0 +1,58 @@
|
||||
Tinytest.add("minimongo - wrapTransform", function (test) {
|
||||
var wrap = LocalCollection.wrapTransform;
|
||||
|
||||
// Transforming no function gives falsey.
|
||||
test.isFalse(wrap(undefined));
|
||||
test.isFalse(wrap(null));
|
||||
|
||||
// It's OK if you don't change the ID.
|
||||
var validTransform = function (doc) {
|
||||
delete doc.x;
|
||||
doc.y = 42;
|
||||
doc.z = function () { return 43; };
|
||||
return doc;
|
||||
};
|
||||
var transformed = wrap(validTransform)({_id: "asdf", x: 54});
|
||||
test.equal(_.keys(transformed), ['_id', 'y', 'z']);
|
||||
test.equal(transformed.y, 42);
|
||||
test.equal(transformed.z(), 43);
|
||||
|
||||
// Ensure that ObjectIDs work (even if the _ids in question are not ===-equal)
|
||||
var oid1 = new LocalCollection._ObjectID();
|
||||
var oid2 = new LocalCollection._ObjectID(oid1.toHexString());
|
||||
test.equal(wrap(function () {return {_id: oid2};})({_id: oid1}),
|
||||
{_id: oid2});
|
||||
|
||||
// transform functions must return objects
|
||||
var invalidObjects = [
|
||||
"asdf", new LocalCollection._ObjectID(), false, null, true,
|
||||
27, [123], /adsf/, new Date, function () {}, undefined
|
||||
];
|
||||
_.each(invalidObjects, function (invalidObject) {
|
||||
var wrapped = wrap(function () { return invalidObject; });
|
||||
test.throws(function () {
|
||||
wrapped({_id: "asdf"});
|
||||
});
|
||||
}, /transform must return object/);
|
||||
|
||||
// transform functions may not change _ids
|
||||
var wrapped = wrap(function (doc) { doc._id = 'x'; return doc; });
|
||||
test.throws(function () {
|
||||
wrapped({_id: 'y'});
|
||||
}, /can't have different _id/);
|
||||
|
||||
// transform functions may remove _ids
|
||||
test.equal({_id: 'a', x: 2},
|
||||
wrap(function (d) {delete d._id; return d;})({_id: 'a', x: 2}));
|
||||
|
||||
// test that wrapped transform functions are nonreactive
|
||||
var unwrapped = function (doc) {
|
||||
test.isFalse(Deps.active);
|
||||
return doc;
|
||||
};
|
||||
var handle = Deps.autorun(function () {
|
||||
test.isTrue(Deps.active);
|
||||
wrap(unwrapped)({_id: "xxx"});
|
||||
});
|
||||
handle.stop();
|
||||
});
|
||||
@@ -38,11 +38,7 @@ Meteor.Collection = function (name, options) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (options.transform) {
|
||||
self._transform = Deps._makeNonreactive(Package.minimongo.LocalCollection.wrapTransform(options.transform));
|
||||
} else {
|
||||
self._transform = null;
|
||||
}
|
||||
self._transform = LocalCollection.wrapTransform(options.transform);
|
||||
|
||||
if (!name && (name !== null)) {
|
||||
Meteor._debug("Warning: creating anonymous collection. It will not be " +
|
||||
@@ -555,12 +551,16 @@ Meteor.Collection.ObjectID = LocalCollection._ObjectID;
|
||||
if (!(options[name] instanceof Function)) {
|
||||
throw new Error(allowOrDeny + ": Value for `" + name + "` must be a function");
|
||||
}
|
||||
if (self._transform && options.transform !== null)
|
||||
options[name].transform = self._transform;
|
||||
if (options.transform)
|
||||
options[name].transform = Deps._makeNonreactive(options.transform);
|
||||
if (options[name].transform)
|
||||
options[name].transform = Package.minimongo.LocalCollection.wrapTransform(options[name].transform);
|
||||
|
||||
// If the transform is specified at all (including as 'null') in this
|
||||
// call, then take that; otherwise, take the transform from the
|
||||
// collection.
|
||||
if (options.transform === undefined) {
|
||||
options[name].transform = self._transform; // already wrapped
|
||||
} else {
|
||||
options[name].transform = LocalCollection.wrapTransform(
|
||||
options.transform);
|
||||
}
|
||||
|
||||
self._validators[name][allowOrDeny].push(options[name]);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ LocalCollectionDriver = function () {
|
||||
|
||||
var ensureCollection = function (name, collections) {
|
||||
if (!(name in collections))
|
||||
collections[name] = new LocalCollection(name);
|
||||
collections[name] = new LocalCollection({name: name});
|
||||
return collections[name];
|
||||
};
|
||||
|
||||
|
||||
@@ -778,9 +778,8 @@ var SynchronousCursor = function (dbCursor, cursorDescription, options) {
|
||||
// inside a user-visible Cursor, we want to provide the outer cursor!
|
||||
self._selfForIteration = options.selfForIteration || self;
|
||||
if (options.useTransform && cursorDescription.options.transform) {
|
||||
self._transform = Deps._makeNonreactive(
|
||||
Package.minimongo.LocalCollection.wrapTransform(
|
||||
cursorDescription.options.transform));
|
||||
self._transform = LocalCollection.wrapTransform(
|
||||
cursorDescription.options.transform);
|
||||
} else {
|
||||
self._transform = null;
|
||||
}
|
||||
|
||||
@@ -818,42 +818,6 @@ testAsyncMulti('mongo-livedata - document with a date, ' + idGeneration, [
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti('mongo-livedata - transform must return an object, ' + idGeneration, [
|
||||
function (test, expect) {
|
||||
var self = this;
|
||||
var justId = function (doc) {
|
||||
return doc._id;
|
||||
};
|
||||
var idInArray = function (doc) {
|
||||
return [doc._id];
|
||||
};
|
||||
TRANSFORMS["justId"] = justId;
|
||||
var collectionOptions = {
|
||||
idGeneration: idGeneration,
|
||||
transform: justId,
|
||||
transformName: "justId"
|
||||
};
|
||||
var collectionName = Random.id();
|
||||
if (Meteor.isClient) {
|
||||
Meteor.call('createInsecureCollection', collectionName, collectionOptions);
|
||||
Meteor.subscribe('c-' + collectionName);
|
||||
}
|
||||
self.coll = new Meteor.Collection(collectionName, collectionOptions);
|
||||
self.coll.insert({}, expect(function (err, id) {
|
||||
test.isFalse(err);
|
||||
test.isTrue(id);
|
||||
test.throws(function () {
|
||||
self.coll.findOne();
|
||||
});
|
||||
test.throws(function () {
|
||||
self.coll.findOne({}, {transform: idInArray});
|
||||
});
|
||||
// you can still override the transform though.
|
||||
test.equal(self.coll.findOne({}, {transform: null}), {_id: id});
|
||||
}));
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti('mongo-livedata - document goes through a transform, ' + idGeneration, [
|
||||
function (test, expect) {
|
||||
var self = this;
|
||||
@@ -915,37 +879,6 @@ testAsyncMulti('mongo-livedata - document goes through a transform, ' + idGenera
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti('mongo-livedata - transformed object can\'t have conflicting _id, ' + idGeneration, [
|
||||
function (test, expect) {
|
||||
var self = this;
|
||||
var justId = function (doc) {
|
||||
doc._id = "foo";
|
||||
return doc;
|
||||
};
|
||||
TRANSFORMS["justId"] = justId;
|
||||
var collectionOptions = {
|
||||
idGeneration: idGeneration,
|
||||
transform: justId,
|
||||
transformName: "justId"
|
||||
};
|
||||
var collectionName = Random.id();
|
||||
if (Meteor.isClient) {
|
||||
Meteor.call('createInsecureCollection', collectionName, collectionOptions);
|
||||
Meteor.subscribe('c-' + collectionName);
|
||||
}
|
||||
self.coll = new Meteor.Collection(collectionName, collectionOptions);
|
||||
self.coll.insert({}, expect(function (err, id) {
|
||||
test.isFalse(err);
|
||||
test.isTrue(id);
|
||||
test.throws(function () {
|
||||
self.coll.findOne();
|
||||
});
|
||||
// you can still override the transform though.
|
||||
test.equal(self.coll.findOne({}, {transform: null})._id, id);
|
||||
}));
|
||||
}
|
||||
]);
|
||||
|
||||
testAsyncMulti('mongo-livedata - transform sets _id if not present, ' + idGeneration, [
|
||||
function (test, expect) {
|
||||
var self = this;
|
||||
|
||||
@@ -30,13 +30,24 @@ OplogObserveDriver = function (options) {
|
||||
|
||||
self._registerPhaseChange(PHASE.QUERYING);
|
||||
|
||||
self._published = new LocalCollection._IdMap;
|
||||
// A minimongo LocalCollection containing the docs that match the selector,
|
||||
// and maybe more. It is guaranteed to contain all the fields needed for the
|
||||
// selector and the projection, and may have other fields too. (In the future
|
||||
// we may try to make this collection be shared between multiple
|
||||
// OplogObserveDrivers, but not currently.)
|
||||
self._collection =
|
||||
new LocalCollection({_observeCallbacksWillNeverYield: true});
|
||||
// XXX think about what all the options are
|
||||
var minimongoCursor = self._collection.find(
|
||||
self._cursorDescription.selector, self._cursorDescription.options);
|
||||
self._stopHandles.push(minimongoCursor.observeChanges(self._multiplexer));
|
||||
|
||||
var selector = self._cursorDescription.selector;
|
||||
self._matcher = options.matcher;
|
||||
var projection = self._cursorDescription.options.fields || {};
|
||||
self._projectionFn = LocalCollection._compileProjection(projection);
|
||||
|
||||
// Projection function, result of combining important fields for selector and
|
||||
// existing fields projection
|
||||
var projection = self._cursorDescription.options.fields || {};
|
||||
self._sharedProjection = self._matcher.combineIntoProjection(projection);
|
||||
self._sharedProjectionFn = LocalCollection._compileProjection(
|
||||
self._sharedProjection);
|
||||
@@ -109,47 +120,51 @@ OplogObserveDriver = function (options) {
|
||||
_.extend(OplogObserveDriver.prototype, {
|
||||
_add: function (doc) {
|
||||
var self = this;
|
||||
var id = doc._id;
|
||||
var fields = _.clone(doc);
|
||||
delete fields._id;
|
||||
if (self._published.has(id))
|
||||
throw Error("tried to add something already published " + id);
|
||||
self._published.set(id, self._sharedProjectionFn(fields));
|
||||
self._multiplexer.added(id, self._projectionFn(fields));
|
||||
doc = self._sharedProjectionFn(doc);
|
||||
// XXX does _sharedProjection always preserve id?
|
||||
if (!_.has(doc, '_id'))
|
||||
throw Error("Can't add doc without _id");
|
||||
self._collection.insert(doc);
|
||||
},
|
||||
_remove: function (id) {
|
||||
_remove: function (id, options) {
|
||||
var self = this;
|
||||
if (!self._published.has(id))
|
||||
options = options || {};
|
||||
var removed = self._collection.remove({_id: id});
|
||||
if (options.mustExist && removed !== 1)
|
||||
throw Error("tried to remove something unpublished " + id);
|
||||
self._published.remove(id);
|
||||
self._multiplexer.removed(id);
|
||||
},
|
||||
_handleDoc: function (id, newDoc, mustMatchNow) {
|
||||
var self = this;
|
||||
newDoc = _.clone(newDoc);
|
||||
newDoc = _.clone(newDoc); // *shallow* clone
|
||||
|
||||
// XXX this is just about "matching selector", not about skip/limit
|
||||
var matchesNow = newDoc && self._matcher.documentMatches(newDoc).result;
|
||||
if (mustMatchNow && !matchesNow) {
|
||||
throw Error("expected " + EJSON.stringify(newDoc) + " to match "
|
||||
+ EJSON.stringify(self._cursorDescription));
|
||||
}
|
||||
|
||||
var matchedBefore = self._published.has(id);
|
||||
var inCollection = !!self._collection.find(id).count();
|
||||
|
||||
if (matchesNow && !matchedBefore) {
|
||||
if (matchesNow && !inCollection) {
|
||||
// It matches the selector and it isn't in our collection, so add it.
|
||||
// XXX once we add skip/limit, this may not always send an added, and
|
||||
// we may need to do some GC
|
||||
self._add(newDoc);
|
||||
} else if (matchedBefore && !matchesNow) {
|
||||
self._remove(id);
|
||||
} else if (inCollection && !matchesNow) {
|
||||
// We remove this from the collection to achieve two goals: (a) causing
|
||||
// the observeChanges to fire removed() and (b) saving memory. That said,
|
||||
// it would be legitimate (if !!newDoc) to update the collection instead
|
||||
// of removing, if we thought we might need this doc again soon.
|
||||
self._remove(id, {mustExist: true});
|
||||
} else if (matchesNow) {
|
||||
var oldDoc = self._published.get(id);
|
||||
if (!oldDoc)
|
||||
throw Error("thought that " + id + " was there!");
|
||||
delete newDoc._id;
|
||||
self._published.set(id, self._sharedProjectionFn(newDoc));
|
||||
var changed = LocalCollection._makeChangedFields(_.clone(newDoc), oldDoc);
|
||||
changed = self._projectionFn(changed);
|
||||
if (!_.isEmpty(changed))
|
||||
self._multiplexer.changed(id, changed);
|
||||
// Replace the doc inside our collection, which may trigger a changed
|
||||
// callback.
|
||||
newDoc = self._sharedProjectionFn(newDoc);
|
||||
// XXX does _sharedProjection always preserve id?
|
||||
if (!_.has(newDoc, '_id'))
|
||||
throw Error("Can't add newDoc without _id");
|
||||
self._collection.update(id, newDoc);
|
||||
}
|
||||
},
|
||||
_fetchModifiedDocuments: function () {
|
||||
@@ -226,16 +241,16 @@ _.extend(OplogObserveDriver.prototype, {
|
||||
// If we're already fetching this one, or about to, we can't optimize; make
|
||||
// sure that we fetch it again if necessary.
|
||||
if (self._phase === PHASE.FETCHING &&
|
||||
(self._currentlyFetching.has(id) || self._needToFetch.has(id))) {
|
||||
((self._currentlyFetching && self._currentlyFetching.has(id)) ||
|
||||
self._needToFetch.has(id))) {
|
||||
self._needToFetch.set(id, op.ts.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (op.op === 'd') {
|
||||
if (self._published.has(id))
|
||||
self._remove(id);
|
||||
self._remove(id);
|
||||
} else if (op.op === 'i') {
|
||||
if (self._published.has(id))
|
||||
if (self._collection.find(id).count())
|
||||
throw new Error("insert found for already-existing ID");
|
||||
|
||||
// XXX what if selector yields? for now it can't but later it could have
|
||||
@@ -257,18 +272,24 @@ _.extend(OplogObserveDriver.prototype, {
|
||||
|
||||
if (isReplace) {
|
||||
self._handleDoc(id, _.extend({_id: id}, op.o));
|
||||
} else if (self._published.has(id) && canDirectlyModifyDoc) {
|
||||
// Oh great, we actually know what the document is, so we can apply
|
||||
// this directly.
|
||||
var newDoc = EJSON.clone(self._published.get(id));
|
||||
newDoc._id = id;
|
||||
LocalCollection._modify(newDoc, op.o);
|
||||
self._handleDoc(id, self._sharedProjectionFn(newDoc));
|
||||
} else if (!canDirectlyModifyDoc ||
|
||||
self._matcher.canBecomeTrueByModifier(op.o)) {
|
||||
self._needToFetch.set(id, op.ts.toString());
|
||||
if (self._phase === PHASE.STEADY)
|
||||
self._fetchModifiedDocuments();
|
||||
} else {
|
||||
var newDoc = self._collection.findOne(id);
|
||||
if (newDoc && canDirectlyModifyDoc) {
|
||||
// Oh great, we actually know what the document is, so we can apply
|
||||
// this directly.
|
||||
// XXX just send the modifier to _collection.update? but then
|
||||
// we don't necessarily get to GC
|
||||
|
||||
// We can avoid another deep clone here since the findOne above would
|
||||
// return a copy anyways
|
||||
LocalCollection._modify(newDoc, op.o);
|
||||
self._handleDoc(id, newDoc);
|
||||
} else if (!canDirectlyModifyDoc ||
|
||||
self._matcher.canBecomeTrueByModifier(op.o)) {
|
||||
self._needToFetch.set(id, op.ts.toString());
|
||||
if (self._phase === PHASE.STEADY)
|
||||
self._fetchModifiedDocuments();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw Error("XXX SURPRISING OPERATION: " + op);
|
||||
@@ -317,18 +338,19 @@ _.extend(OplogObserveDriver.prototype, {
|
||||
self._currentlyFetching = null;
|
||||
++self._fetchGeneration; // ignore any in-flight fetches
|
||||
self._registerPhaseChange(PHASE.QUERYING);
|
||||
self._collection.pauseObservers();
|
||||
// XXX this won't be quite correct for skip/limit
|
||||
self._collection.remove({});
|
||||
|
||||
// Defer so that we don't block.
|
||||
Meteor.defer(function () {
|
||||
// subtle note: _published does not contain _id fields, but newResults
|
||||
// does
|
||||
var newResults = new LocalCollection._IdMap;
|
||||
var cursor = self._cursorForQuery();
|
||||
cursor.forEach(function (doc) {
|
||||
newResults.set(doc._id, doc);
|
||||
// Insert all the documents currently found by the query.
|
||||
self._cursorForQuery().forEach(function (doc) {
|
||||
self._collection.insert(doc);
|
||||
});
|
||||
|
||||
self._publishNewResults(newResults);
|
||||
// Allow observe callbacks (ie multiplexer invocations) to fire.
|
||||
self._collection.resumeObservers();
|
||||
|
||||
self._doneQuerying();
|
||||
});
|
||||
@@ -398,34 +420,6 @@ _.extend(OplogObserveDriver.prototype, {
|
||||
},
|
||||
|
||||
|
||||
// Replace self._published with newResults (both are IdMaps), invoking observe
|
||||
// callbacks on the multiplexer.
|
||||
//
|
||||
// XXX This is very similar to LocalCollection._diffQueryUnorderedChanges. We
|
||||
// should really: (a) Unify IdMap and OrderedDict into Unordered/OrderedDict (b)
|
||||
// Rewrite diff.js to use these classes instead of arrays and objects.
|
||||
_publishNewResults: function (newResults) {
|
||||
var self = this;
|
||||
|
||||
// First remove anything that's gone. Be careful not to modify
|
||||
// self._published while iterating over it.
|
||||
var idsToRemove = [];
|
||||
self._published.forEach(function (doc, id) {
|
||||
if (!newResults.has(id))
|
||||
idsToRemove.push(id);
|
||||
});
|
||||
_.each(idsToRemove, function (id) {
|
||||
self._remove(id);
|
||||
});
|
||||
|
||||
// Now do adds and changes.
|
||||
newResults.forEach(function (doc, id) {
|
||||
// "true" here means to throw if we think this doc doesn't match the
|
||||
// selector.
|
||||
self._handleDoc(id, doc, true);
|
||||
});
|
||||
},
|
||||
|
||||
// This stop function is invoked from the onStop of the ObserveMultiplexer, so
|
||||
// it shouldn't actually be possible to call it until the multiplexer is
|
||||
// ready.
|
||||
@@ -449,7 +443,6 @@ _.extend(OplogObserveDriver.prototype, {
|
||||
self._writesToCommitWhenWeReachSteady = null;
|
||||
|
||||
// Proactively drop references to potentially big things.
|
||||
self._published = null;
|
||||
self._needToFetch = null;
|
||||
self._currentlyFetching = null;
|
||||
self._oplogEntryHandle = null;
|
||||
@@ -463,6 +456,9 @@ _.extend(OplogObserveDriver.prototype, {
|
||||
var self = this;
|
||||
var now = new Date;
|
||||
|
||||
if (phase === self._phase)
|
||||
return;
|
||||
|
||||
if (self._phase) {
|
||||
var timeDiff = now - self._phaseStartTime;
|
||||
Package.facts && Package.facts.Facts.incrementServerFact(
|
||||
|
||||
@@ -106,7 +106,7 @@ Tinytest.add("spacebars - compiler output", function (test) {
|
||||
function() {
|
||||
var self = this;
|
||||
return Spacebars.include(
|
||||
Template.foo || self.lookup("foo"), null, {
|
||||
self.lookup("foo", {template: true}), null, {
|
||||
content: UI.block(function() {
|
||||
var self = this;
|
||||
return "abc";
|
||||
@@ -132,7 +132,7 @@ Tinytest.add("spacebars - compiler output", function (test) {
|
||||
function() {
|
||||
var self = this;
|
||||
return Spacebars.include(
|
||||
Template.foo || self.lookup("foo"),
|
||||
self.lookup("foo", {template: true}),
|
||||
function () {
|
||||
return Spacebars.call(self.lookup("bar"));
|
||||
});
|
||||
@@ -142,7 +142,7 @@ Tinytest.add("spacebars - compiler output", function (test) {
|
||||
function() {
|
||||
var self = this;
|
||||
return Spacebars.include(
|
||||
Template.foo || self.lookup("foo"),
|
||||
self.lookup("foo", {template: true}),
|
||||
function () {
|
||||
return { x: Spacebars.call(self.lookup("bar")) };
|
||||
});
|
||||
@@ -152,7 +152,7 @@ Tinytest.add("spacebars - compiler output", function (test) {
|
||||
function() {
|
||||
var self = this;
|
||||
return Spacebars.include(
|
||||
Template.foo || self.lookup("foo"),
|
||||
self.lookup("foo", {template: true}),
|
||||
function () {
|
||||
return Spacebars.call(Spacebars.dot(self.lookup("bar"), "baz"));
|
||||
});
|
||||
@@ -162,7 +162,7 @@ Tinytest.add("spacebars - compiler output", function (test) {
|
||||
function() {
|
||||
var self = this;
|
||||
return Spacebars.include(
|
||||
Template.foo || self.lookup("foo"),
|
||||
self.lookup("foo", {template: true}),
|
||||
function () {
|
||||
return {
|
||||
x: Spacebars.call(Spacebars.dot(self.lookup("bar"), "baz"))
|
||||
@@ -174,7 +174,7 @@ Tinytest.add("spacebars - compiler output", function (test) {
|
||||
function() {
|
||||
var self = this;
|
||||
return Spacebars.include(
|
||||
Template.foo || self.lookup("foo"),
|
||||
self.lookup("foo", {template: true}),
|
||||
function () {
|
||||
return Spacebars.dataMustache(self.lookup("bar"),
|
||||
self.lookup("baz"));
|
||||
@@ -185,7 +185,7 @@ Tinytest.add("spacebars - compiler output", function (test) {
|
||||
function() {
|
||||
var self = this;
|
||||
return Spacebars.include(
|
||||
Template.foo || self.lookup("foo"),
|
||||
self.lookup("foo", {template: true}),
|
||||
function () {
|
||||
return Spacebars.dataMustache(self.lookup("bar"),
|
||||
self.lookup("baz"));
|
||||
@@ -202,7 +202,7 @@ Tinytest.add("spacebars - compiler output", function (test) {
|
||||
function() {
|
||||
var self = this;
|
||||
return Spacebars.include(
|
||||
Template.foo || self.lookup("foo"),
|
||||
self.lookup("foo", {template: true}),
|
||||
function () {
|
||||
return Spacebars.dataMustache(
|
||||
Spacebars.dot(self.lookup("p"), "q"),
|
||||
@@ -296,4 +296,4 @@ Tinytest.add("spacebars - compiler errors", function (test) {
|
||||
"First argument must be a function");
|
||||
isError("{{#foo 0 x=0}}{{/foo}}",
|
||||
"First argument must be a function");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -203,17 +203,9 @@ var codeGenTemplateTag = function (tag) {
|
||||
builtInBlockHelpers[path[0]] + '(' + callArgs.join(', ') + ')');
|
||||
|
||||
} else {
|
||||
var compCode = codeGenPath(path);
|
||||
var compCode = codeGenPath(path, {lookupTemplate: true});
|
||||
|
||||
if (path.length === 1) {
|
||||
// toObjectLiteralKey returns `"foo"` or `foo` depending on
|
||||
// whether `foo` is a safe JavaScript identifier.
|
||||
var member = toObjectLiteralKey(path[0]);
|
||||
var templateDotFoo = (member.charAt(0) === '"' ?
|
||||
'Template[' + member + ']' :
|
||||
'Template.' + member);
|
||||
compCode = ('(' + templateDotFoo + ' || ' + compCode + ')');
|
||||
} else {
|
||||
if (path.length !== 1) {
|
||||
// path code may be reactive; wrap it
|
||||
compCode = 'function () { return ' + compCode + '; }';
|
||||
}
|
||||
@@ -252,7 +244,14 @@ var makeObjectLiteral = function (obj) {
|
||||
// (i.e. it may invalidate the current computation).
|
||||
//
|
||||
// No code is generated to call the result if it's a function.
|
||||
var codeGenPath = function (path) {
|
||||
//
|
||||
// Options:
|
||||
//
|
||||
// - lookupTemplate {Boolean} If true, generated code also looks in
|
||||
// the list of templates. (After helpers, before data context).
|
||||
// Used when generating code for `{{> foo}}` or `{{#foo}}`. Only
|
||||
// used for non-dotted paths.
|
||||
var codeGenPath = function (path, opts) {
|
||||
if (builtInBlockHelpers.hasOwnProperty(path[0]))
|
||||
throw new Error("Can't use the built-in '" + path[0] + "' here");
|
||||
// Let `{{#if content}}` check whether this template was invoked via
|
||||
@@ -264,7 +263,10 @@ var codeGenPath = function (path) {
|
||||
return builtInLexicals[path[0]];
|
||||
}
|
||||
|
||||
var code = 'self.lookup(' + toJSLiteral(path[0]) + ')';
|
||||
var args = [toJSLiteral(path[0])];
|
||||
if (opts && opts.lookupTemplate && path.length === 1)
|
||||
args.push('{template: true}');
|
||||
var code = 'self.lookup(' + args.join(', ') + ')';
|
||||
|
||||
if (path.length > 1) {
|
||||
code = 'Spacebars.dot(' + code + ', ' +
|
||||
|
||||
@@ -448,3 +448,20 @@ Hi there!
|
||||
<template name="spacebars_template_test_double">
|
||||
{{foo}}
|
||||
</template>
|
||||
|
||||
<template name="spacebars_template_test_inclusion_lookup">
|
||||
{{> spacebars_template_test_inclusion_lookup_subtmpl}}
|
||||
{{> dataContextSubtmpl}}
|
||||
</template>
|
||||
|
||||
<template name="spacebars_template_test_inclusion_lookup_subtmpl">
|
||||
This is the template.
|
||||
</template>
|
||||
|
||||
<template name="spacebars_template_test_inclusion_lookup_subtmpl2">
|
||||
This is generated by a helper with the same name.
|
||||
</template>
|
||||
|
||||
<template name="spacebars_template_test_inclusion_lookup_subtmpl3">
|
||||
This is a template passed in the data context.
|
||||
</template>
|
||||
|
||||
@@ -1340,3 +1340,28 @@ Tinytest.add("spacebars - templates - double", function (test) {
|
||||
run(null, '');
|
||||
run(undefined, '');
|
||||
});
|
||||
|
||||
Tinytest.add("spacebars - templates - inclusion lookup order", function (test) {
|
||||
// test that {{> foo}} looks for a helper named 'foo', then a
|
||||
// template named 'foo', then a 'foo' field in the data context.
|
||||
var tmpl = Template.spacebars_template_test_inclusion_lookup;
|
||||
tmpl.data = function () {
|
||||
return {
|
||||
// shouldn't have an effect since we define a helper with the
|
||||
// same name.
|
||||
spacebars_template_test_inclusion_lookup_subtmpl: Template.
|
||||
spacebars_template_test_inclusion_lookup_subtmpl3,
|
||||
dataContextSubtmpl: Template.
|
||||
spacebars_template_test_inclusion_lookup_subtmpl3};
|
||||
};
|
||||
|
||||
tmpl.spacebars_template_test_inclusion_lookup_subtmpl =
|
||||
Template.spacebars_template_test_inclusion_lookup_subtmpl2;
|
||||
|
||||
var lines = _.map(
|
||||
stripComments(renderToDiv(tmpl).innerHTML).split('\n'),
|
||||
trim);
|
||||
test.equal(lines,
|
||||
["This is generated by a helper with the same name.",
|
||||
"This is a template passed in the data context."]);
|
||||
});
|
||||
|
||||
@@ -29,8 +29,13 @@ var builtInComponents = {
|
||||
};
|
||||
|
||||
_extend(UI.Component, {
|
||||
lookup: function (id) {
|
||||
// Options:
|
||||
//
|
||||
// - template {Boolean} If true, look at the list of templates after
|
||||
// helpers and before data context.
|
||||
lookup: function (id, opts) {
|
||||
var self = this;
|
||||
var template = opts && opts.template;
|
||||
var result;
|
||||
var comp;
|
||||
|
||||
@@ -58,6 +63,7 @@ _extend(UI.Component, {
|
||||
|
||||
} else if (_.has(builtInComponents, id)) {
|
||||
return builtInComponents[id];
|
||||
|
||||
// Code to search the global namespace for capitalized names
|
||||
// like component classes, `Template`, `StringUtils.foo`,
|
||||
// etc.
|
||||
@@ -79,6 +85,10 @@ _extend(UI.Component, {
|
||||
// for this? We should definitely not put it on the Handlebars
|
||||
// namespace.
|
||||
result = Handlebars._globalHelpers[id];
|
||||
|
||||
} else if (template && _.has(Template, id)) {
|
||||
return Template[id];
|
||||
|
||||
} else {
|
||||
// Resolve id `foo` as `data.foo` (with a "soft dot").
|
||||
return function (/*arguments*/) {
|
||||
|
||||
1
packages/underscore-tests/.gitignore
vendored
Normal file
1
packages/underscore-tests/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.build*
|
||||
45
packages/underscore-tests/each_test.js
Normal file
45
packages/underscore-tests/each_test.js
Normal file
@@ -0,0 +1,45 @@
|
||||
Tinytest.add("underscore - each", function (test) {
|
||||
// arrays
|
||||
_.each([42], function (val, index) {
|
||||
test.equal(index, 0);
|
||||
test.equal(val, 42);
|
||||
});
|
||||
|
||||
// objects with 'length' field aren't treated as arrays
|
||||
_.each({length: 42}, function (val, key) {
|
||||
test.equal(key, 'length');
|
||||
test.equal(val, 42);
|
||||
});
|
||||
|
||||
// The special 'arguments' variable is treated as an
|
||||
// array
|
||||
(function () {
|
||||
_.each(arguments, function (val, index) {
|
||||
test.equal(index, 0);
|
||||
test.equal(val, 42);
|
||||
});
|
||||
})(42);
|
||||
|
||||
// An object with a 'callee' field isn't treated as arguments
|
||||
_.each({callee: 42}, function (val, key) {
|
||||
test.equal(key, 'callee');
|
||||
test.equal(val, 42);
|
||||
});
|
||||
|
||||
// An object with a 'callee' field isn't treated as arguments
|
||||
_.each({length: 4, callee: 42}, function (val, key) {
|
||||
if (key === 'callee')
|
||||
test.equal(val, 42);
|
||||
else if (key === 'length')
|
||||
test.equal(val, 4);
|
||||
else
|
||||
test.fail({message: 'unexpected key: ' + key});
|
||||
});
|
||||
|
||||
|
||||
// NOTE: An object with a numberic 'length' field *and* a function
|
||||
// 'callee' field will be treated as an array in IE. This may or may
|
||||
// not be fixable, but isn't a big deal since: (1) 'callee' is a
|
||||
// pretty rare key, and (2) JSON objects can't have functions
|
||||
// anyways, which is the main use-case for _.each.
|
||||
});
|
||||
10
packages/underscore-tests/package.js
Normal file
10
packages/underscore-tests/package.js
Normal file
@@ -0,0 +1,10 @@
|
||||
Package.describe({
|
||||
// These tests can't be directly in the underscore packages since
|
||||
// Tinytest depends on underscore
|
||||
summary: "Tests for the underscore package"
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use(['tinytest', 'underscore']);
|
||||
api.add_files('each_test.js');
|
||||
});
|
||||
@@ -70,6 +70,21 @@
|
||||
// Collection Functions
|
||||
// --------------------
|
||||
|
||||
// METEOR CHANGE: Define _isArguments instead of depending on
|
||||
// _.isArguments which is defined using each. In looksLikeArray
|
||||
// (which each depends on), we then use _isArguments instead of
|
||||
// _.isArguments.
|
||||
var _isArguments = function (obj) {
|
||||
return toString.call(obj) === '[object Arguments]';
|
||||
};
|
||||
// Define a fallback version of the method in browsers (ahem, IE), where
|
||||
// there isn't any inspectable "Arguments" type.
|
||||
if (!_isArguments(arguments)) {
|
||||
_isArguments = function (obj) {
|
||||
return !!(obj && hasOwnProperty.call(obj, 'callee') && typeof obj.callee === 'function');
|
||||
};
|
||||
}
|
||||
|
||||
// METEOR CHANGE: _.each({length: 5}) should be treated like an object, not an
|
||||
// array. This looksLikeArray function is introduced by Meteor, and replaces
|
||||
// all instances of `obj.length === +obj.length`.
|
||||
@@ -77,7 +92,8 @@
|
||||
// https://github.com/jashkenas/underscore/issues/770
|
||||
var looksLikeArray = function (obj) {
|
||||
return (obj.length === +obj.length
|
||||
&& (_.isArguments(obj) || obj.constructor !== Object));
|
||||
// _.isArguments not yet necessarily defined here
|
||||
&& (_isArguments(obj) || obj.constructor !== Object));
|
||||
};
|
||||
|
||||
// The cornerstone, an `each` implementation, aka `forEach`.
|
||||
|
||||
@@ -109,11 +109,7 @@ npm install eachline@2.4.0
|
||||
npm install source-map@0.1.31
|
||||
npm install source-map-support@0.2.5
|
||||
npm install bcrypt@0.7.7
|
||||
|
||||
# Based on 1.0.1; includes our PRs
|
||||
# https://github.com/nodejitsu/node-http-proxy/pull/561 and
|
||||
# https://github.com/nodejitsu/node-http-proxy/pull/560
|
||||
npm install https://github.com/meteor/node-http-proxy/tarball/d8ea687936d6bed0f3e99849695cab2dcdccd6f4
|
||||
npm install http-proxy@1.0.2
|
||||
|
||||
# Using the unreleased 1.1 branch. We can probably switch to a built NPM version
|
||||
# when it gets released.
|
||||
|
||||
Reference in New Issue
Block a user