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:
David Greenspan
2014-01-29 18:30:31 -08:00
29 changed files with 556 additions and 342 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -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.

View File

@@ -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

View File

@@ -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;
}
});

View File

@@ -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']);

View 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;
}
});

View File

@@ -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();
};

View File

@@ -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();
});

View File

@@ -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;
};

View File

@@ -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');
});

View File

@@ -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);

View File

@@ -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 {

View 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();
});

View File

@@ -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]);
}

View File

@@ -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];
};

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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(

View File

@@ -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");
});
});

View File

@@ -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 + ', ' +

View File

@@ -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>

View File

@@ -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."]);
});

View File

@@ -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
View File

@@ -0,0 +1 @@
.build*

View 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.
});

View 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');
});

View File

@@ -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`.

View File

@@ -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.