mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'livedata-cleanups'
This commit is contained in:
@@ -94,8 +94,8 @@ Template.create_list.events = {
|
||||
var target = $(evt.target);
|
||||
var text = target.val();
|
||||
if (evt.keyCode === 13 && text) {
|
||||
var list = Lists.insert({name: text});
|
||||
Router.setList(list._id);
|
||||
var id = Lists.insert({name: text});
|
||||
Router.setList(id);
|
||||
target.val('');
|
||||
}
|
||||
}
|
||||
|
||||
2
examples/todos/server/bootstrap.js
vendored
2
examples/todos/server/bootstrap.js
vendored
@@ -40,7 +40,7 @@ Meteor.startup(function () {
|
||||
|
||||
var timestamp = (new Date()).getTime();
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var list_id = Lists.insert({name: data[i].name})._id;
|
||||
var list_id = Lists.insert({name: data[i].name});
|
||||
for (var j = 0; j < data[i].contents.length; j++) {
|
||||
var info = data[i].contents[j];
|
||||
Todos.insert({list_id: list_id,
|
||||
|
||||
@@ -30,10 +30,10 @@ Template.add_room.events = {
|
||||
'click': function () {
|
||||
// XXX should put up dialog to get name
|
||||
// XXX should support automatically set created/updated timestamps
|
||||
var obj = Rooms.insert({name: "New room",
|
||||
// XXX horrid syntax
|
||||
created: (new Date()).getTime()});
|
||||
selectRoom(obj._id);
|
||||
var room_id = Rooms.insert({name: "New room",
|
||||
// XXX horrid syntax
|
||||
created: (new Date()).getTime()});
|
||||
selectRoom(room_id);
|
||||
// XXX XXX XXX this fails to work -- it leaves edit mode after
|
||||
// 1RTT. what happens is, the server echos the insert back to us,
|
||||
// and that is currently wired up to trigger a changed event on
|
||||
|
||||
@@ -1 +1 @@
|
||||
App.autopublish();
|
||||
Meteor.default_server.autopublish();
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
// XXX this isn't going to work -- when connecting to a remote server,
|
||||
// the user isn't going to know to include /sockjs. need to add it in
|
||||
// stream_client..
|
||||
|
||||
// Path matches sockjs 'prefix' in stream_server. We should revisit this
|
||||
// once we specify the 'on the wire' aspects of livedata more clearly.
|
||||
App = Meteor.connect('/sockjs', true /* restart_on_update */);
|
||||
|
||||
_.extend(Meteor, {
|
||||
status: function () {
|
||||
return App.status();
|
||||
},
|
||||
// XXX this isn't going to work -- when connecting to a remote
|
||||
// server, the user isn't going to know to include /sockjs. need to
|
||||
// add it in stream_client..
|
||||
|
||||
reconnect: function () {
|
||||
return App.reconnect();
|
||||
},
|
||||
|
||||
subscribe: function (/* arguments */) {
|
||||
return App.subscribe.apply(App, _.toArray(arguments));
|
||||
},
|
||||
// Path matches sockjs 'prefix' in stream_server. We should revisit
|
||||
// this once we specify the 'on the wire' aspects of livedata more
|
||||
// clearly.
|
||||
default_connection: Meteor.connect('/sockjs', true /* restart_on_update */),
|
||||
|
||||
refresh: function (notification) {
|
||||
}
|
||||
});
|
||||
|
||||
// Proxy the public methods of Meteor.default_connection so they can
|
||||
// be called directly on Meteor.
|
||||
_.each(['subscribe', 'methods', 'call', 'apply', 'status', 'reconnect'],
|
||||
function (name) {
|
||||
Meteor[name] = _.bind(Meteor.default_connection[name],
|
||||
Meteor.default_connection);
|
||||
});
|
||||
|
||||
@@ -1,2 +1,43 @@
|
||||
// XXX namespacing
|
||||
|
||||
Meteor._MethodInvocation = function (is_simulation, unblock) {
|
||||
var self = this;
|
||||
|
||||
// true if we're running not the actual method, but a stub (that is,
|
||||
// if we're on the client and presently running a simulation of a
|
||||
// server-side method for latency compensation purposes). never true
|
||||
// except in a client such as a browser, since there's no point in
|
||||
// running stubs unless you have a zero-latency connection to the
|
||||
// user.
|
||||
this.is_simulation = is_simulation;
|
||||
|
||||
// call this function to allow other method invocations (from the
|
||||
// same client) to continue running without waiting for this one to
|
||||
// complete.
|
||||
this.unblock = unblock || function () {};
|
||||
};
|
||||
|
||||
Meteor._CurrentInvocation = new Meteor.EnvironmentVariable;
|
||||
|
||||
Meteor.Error = function (error, reason, details) {
|
||||
var self = this;
|
||||
|
||||
// Currently, a numeric code, likely similar to a HTTP code (eg,
|
||||
// 404, 500). That is likely to change though.
|
||||
self.error = error;
|
||||
|
||||
// Optional: A short human-readable summary of the error. Not
|
||||
// intended to be shown to end users, just developers. ("Not Found",
|
||||
// "Internal Server Error")
|
||||
self.reason = reason;
|
||||
|
||||
// Optional: Additional information about the error, say for
|
||||
// debugging. It might be a (textual) stack trace if the server is
|
||||
// willing to provide one. The corresponding thing in HTTP would be
|
||||
// the body of a 404 or 500 response. (The difference is that we
|
||||
// never expect this to be shown to end users, only developers, so
|
||||
// it doesn't need to be pretty.)
|
||||
self.details = details;
|
||||
};
|
||||
|
||||
Meteor.Error.prototype = new Error;
|
||||
@@ -1,66 +1,7 @@
|
||||
Meteor._ClientMethodInvocation = function (name, handler) {
|
||||
var self = this;
|
||||
|
||||
// XXX need: user, setRestartHook, setUser (simulated??)
|
||||
|
||||
self._enclosing = null;
|
||||
self.isSimulation = null;
|
||||
self._name = name;
|
||||
self._handler = handler;
|
||||
self._callback = null;
|
||||
self._id = null;
|
||||
};
|
||||
|
||||
_.extend(Meteor._ClientMethodInvocation.prototype, {
|
||||
beginAsync: function () {
|
||||
// XXX need a much better error message!
|
||||
// duplicated in livedata_server
|
||||
throw new Error("Simulated methods may not be asynchronous");
|
||||
},
|
||||
|
||||
_run: function (args, callback, enqueue) {
|
||||
var self = this;
|
||||
self._enclosing = Meteor._CurrentInvocation.get();
|
||||
self._callback = callback;
|
||||
self.isSimulation = true; // NB: refers to child invocations, not to us
|
||||
|
||||
// run locally (if we have a stub for it)
|
||||
if (self._handler) {
|
||||
try {
|
||||
var ret = Meteor._CurrentInvocation.withValue(self, function () {
|
||||
return self._handler.apply(self, args);
|
||||
});
|
||||
} catch (e) {
|
||||
var stub_exception = e;
|
||||
}
|
||||
}
|
||||
|
||||
if (self._enclosing && self._enclosing.isSimulation) {
|
||||
// In simulation mode, never do an RPC. Use the result of
|
||||
// running the stub instead.
|
||||
if (self._callback) {
|
||||
if (stub_exception)
|
||||
self._callback({error: 500, reason: "Stub threw exception"});
|
||||
else
|
||||
self._callback(ret);
|
||||
}
|
||||
} else {
|
||||
// This invocation is real, not a simulation. Do the RPC.
|
||||
|
||||
// Note that it is important that the function totally complete,
|
||||
// locally, before the message is sent to the server. (Or at
|
||||
// least, we need to guarantee that the snapshot is not restored
|
||||
// until the local copy of the function has stopped doing writes.)
|
||||
|
||||
enqueue({msg: 'method', method: self._name, params: args},
|
||||
self._callback);
|
||||
}
|
||||
|
||||
if (stub_exception)
|
||||
throw stub_exception;
|
||||
return ret;
|
||||
}
|
||||
});
|
||||
if (Meteor.is_server) {
|
||||
// XXX namespacing
|
||||
var Future = __meteor_bootstrap__.require('fibers/future');
|
||||
}
|
||||
|
||||
// list of subscription tokens outstanding during a
|
||||
// captureDependencies run. only set when we're doing a run. The fact
|
||||
@@ -294,7 +235,6 @@ _.extend(Meteor._LivedataConnection.prototype, {
|
||||
apply: function (name, args, callback) {
|
||||
var self = this;
|
||||
var enclosing = Meteor._CurrentInvocation.get();
|
||||
var handler = self.method_handlers[name];
|
||||
|
||||
if (callback)
|
||||
// XXX would it be better form to do the binding in stream.on,
|
||||
@@ -304,20 +244,101 @@ _.extend(Meteor._LivedataConnection.prototype, {
|
||||
Meteor._debug("Exception while delivering result of invoking '" +
|
||||
name + "'", e.stack);
|
||||
});
|
||||
else
|
||||
callback = function () {};
|
||||
|
||||
var enqueue = function (msg, callback) {
|
||||
msg.id = '' + (self.next_method_id++);
|
||||
self.outstanding_methods.push({
|
||||
msg: msg, callback: callback});
|
||||
self.unsatisfied_methods[msg.id] = true;
|
||||
self.stream.send(JSON.stringify(msg));
|
||||
var is_simulation = enclosing && enclosing.is_simulation;
|
||||
if (Meteor.is_client) {
|
||||
// If on a client, run the stub, if we have one. The stub is
|
||||
// supposed to make some temporary writes to the database to
|
||||
// give the user a smooth experience until the actual result of
|
||||
// executing the method comes back from the server (whereupon
|
||||
// the temporary writes to the database will be reversed during
|
||||
// the beginUpdate/endUpdate process.)
|
||||
//
|
||||
// Normally, we ignore the return value of the stub (even if it
|
||||
// is an exception), in favor of the real return value from the
|
||||
// server. The exception is if the *caller* is a stub. In that
|
||||
// case, we're not going to do a RPC, so we use the return value
|
||||
// of the stub as our return value.
|
||||
var stub = self.method_handlers[name];
|
||||
if (stub) {
|
||||
var invocation = new Meteor._MethodInvocation(true /* is_simulation */);
|
||||
try {
|
||||
var ret = Meteor._CurrentInvocation.withValue(invocation,function () {
|
||||
return stub.apply(self, args);
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
var exception = e;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in a simulation, stop and return the result we have,
|
||||
// rather than going on to do an RPC. This can only happen on
|
||||
// the client (since we only bother with stubs and simulations
|
||||
// on the client.) If there was not stub, we'll end up returning
|
||||
// undefined.
|
||||
if (is_simulation) {
|
||||
if (callback) {
|
||||
callback(exception, ret);
|
||||
return;
|
||||
}
|
||||
if (exception)
|
||||
throw exception;
|
||||
return ret;
|
||||
}
|
||||
|
||||
// If an exception occurred in a stub, and we're ignoring it
|
||||
// because we're doing an RPC and want to use what the server
|
||||
// returns instead, log it so the developer knows.
|
||||
//
|
||||
// Tests can set the 'expected' flag on an exception so it won't
|
||||
// go to log.
|
||||
if (exception && !exception.expected)
|
||||
Meteor._debug("Exception while simulating the effect of invoking '" +
|
||||
name + "'", exception.stack);
|
||||
}
|
||||
|
||||
// At this point we're definitely doing an RPC, and we're going to
|
||||
// return the value of the RPC to the caller.
|
||||
|
||||
// If the caller didn't give a callback, decide what to do.
|
||||
if (!callback) {
|
||||
if (Meteor.is_client)
|
||||
// On the client, we don't have fibers, so we can't block. The
|
||||
// only thing we can do is to return undefined and discard the
|
||||
// result of the RPC.
|
||||
callback = function () {};
|
||||
else {
|
||||
// On the server, make the function synchronous.
|
||||
var future = new Future;
|
||||
callback = function (err, result) {
|
||||
future['return']([err, result]);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Send the RPC. Note that on the client, it is important that the
|
||||
// stub have finished before we send the RPC (or at least we need
|
||||
// to guaranteed that the snapshot is not restored until the stub
|
||||
// has stopped doing writes.)
|
||||
var msg = {
|
||||
msg: 'method',
|
||||
method: name,
|
||||
params: args,
|
||||
id: '' + (self.next_method_id++)
|
||||
};
|
||||
self.outstanding_methods.push({msg: msg, callback: callback});
|
||||
self.unsatisfied_methods[msg.id] = true;
|
||||
self.stream.send(JSON.stringify(msg));
|
||||
|
||||
var invocation = new Meteor._ClientMethodInvocation(name, handler, self);
|
||||
// if _run throws an exception, allow it to propagate
|
||||
return invocation._run(args, callback, enqueue);
|
||||
// If we're using the default callback on the server,
|
||||
// synchronously return the result from the remote host.
|
||||
if (future) {
|
||||
var outcome = future.wait();
|
||||
if (outcome[0])
|
||||
throw outcome[0];
|
||||
return outcome[1];
|
||||
}
|
||||
},
|
||||
|
||||
status: function () {
|
||||
@@ -330,11 +351,11 @@ _.extend(Meteor._LivedataConnection.prototype, {
|
||||
return self.stream.reconnect();
|
||||
},
|
||||
|
||||
// called when we are up-to-date with the server. intended for use
|
||||
// only in tests. currently, you are very limited in what you may do
|
||||
// inside your callback -- in particular, don't do anything that
|
||||
// could result in another call to onQuiesce, or results are
|
||||
// undefined.
|
||||
// PRIVATE: called when we are up-to-date with the server. intended
|
||||
// for use only in tests. currently, you are very limited in what
|
||||
// you may do inside your callback -- in particular, don't do
|
||||
// anything that could result in another call to onQuiesce, or
|
||||
// results are undefined.
|
||||
onQuiesce: function (f) {
|
||||
var self = this;
|
||||
|
||||
@@ -465,8 +486,11 @@ _.extend(Meteor._LivedataConnection.prototype, {
|
||||
// callback will have already been bindEnvironment'd by apply(),
|
||||
// so no need to catch exceptions
|
||||
if ('error' in msg)
|
||||
m.callback(msg.error);
|
||||
m.callback(new Meteor.Error(msg.error.error, msg.error.reason,
|
||||
msg.error.details));
|
||||
else
|
||||
// msg.result may be undefined if the method didn't return a
|
||||
// value
|
||||
m.callback(undefined, msg.result);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,139 +1,3 @@
|
||||
/******************************************************************************/
|
||||
/* ServerMethodInvocation */
|
||||
/******************************************************************************/
|
||||
|
||||
Meteor._ServerMethodInvocation = function (name, handler) {
|
||||
var self = this;
|
||||
|
||||
self._enclosing = null;
|
||||
self.isSimulation = null;
|
||||
self._name = name;
|
||||
self._handler = handler;
|
||||
self._async = false;
|
||||
self._responded = false;
|
||||
self._autoresponded = false;
|
||||
self._threw = false;
|
||||
self._callback = null;
|
||||
self._next = null;
|
||||
self._calledNext = false;
|
||||
};
|
||||
|
||||
_.extend(Meteor._ServerMethodInvocation.prototype, {
|
||||
beginAsync: function (okToContinue) {
|
||||
var self = this;
|
||||
|
||||
if (okToContinue === undefined)
|
||||
okToContinue = true;
|
||||
|
||||
if (self.isSimulation)
|
||||
// XXX need a much better error message!
|
||||
// duplicated in livedata_connection
|
||||
throw new Error("Simulated methods may not be asynchronous");
|
||||
else if (self._responded)
|
||||
throw new Error("The method has already returned, so it is too late " +
|
||||
"to mark it as asynchronous");
|
||||
else {
|
||||
self._async = true;
|
||||
if (okToContinue && !self._calledNext) {
|
||||
self._calledNext = true;
|
||||
self._next && self._next();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
respond: function (ret) {
|
||||
this._sendResponse(undefined, ret, "async");
|
||||
},
|
||||
|
||||
error: function (code, message) {
|
||||
this._sendResponse({error: code, reason: message}, undefined, "async");
|
||||
},
|
||||
|
||||
// from: "async", "sync", or "throw".
|
||||
// self._threw is set by _run, and indicates that we're about to
|
||||
// report an exception and not to allow any other fibers kicked off by
|
||||
// the method to emit data.
|
||||
|
||||
_sendResponse: function (error, ret, from) {
|
||||
var self = this;
|
||||
|
||||
if (from === "throw")
|
||||
self._threw = true;
|
||||
|
||||
if (self._threw && from !== "throw")
|
||||
// this is a different fiber. don't emit data, don't print an error.
|
||||
return;
|
||||
|
||||
if (self._responded) {
|
||||
// another fiber already reported a result to the client. if
|
||||
// this fiber is throwing an exception, there's nothing left to do,
|
||||
// since methods can only return a single result. the exception
|
||||
// has already been logged. otherwise, throw an error: the user's
|
||||
// code is responding more than once.
|
||||
if (from === "throw")
|
||||
return;
|
||||
if (self._autoresponded)
|
||||
throw new Error(
|
||||
"The method has already returned, so it is too late to call " +
|
||||
"respond() or error(). If you want to respond asynchronously, " +
|
||||
"first use beginAsync() to prevent a response from being " +
|
||||
"automatically sent.");
|
||||
else
|
||||
throw new Error(
|
||||
"respond() or error() may only be called once per request");
|
||||
}
|
||||
self._responded = true;
|
||||
self._autoresponded = (from === "sync");
|
||||
|
||||
// if we haven't yet yielded to the next method in the queue, do
|
||||
// that now, just before sending the response.
|
||||
if (!self._calledNext) {
|
||||
self._calledNext = true;
|
||||
self._next && self._next();
|
||||
}
|
||||
|
||||
// call the callback. should happen exactly once per method.
|
||||
self._callback && self._callback(error, ret);
|
||||
},
|
||||
|
||||
// entry point
|
||||
// - returns the immediate value (or throws an exception)
|
||||
// - in any case, calls callback (if truthy) with eventual result
|
||||
// - caller should call bindEnvironment on callback, or otherwise handle
|
||||
// any exceptions it throws
|
||||
// - 'name' is for exception reporting
|
||||
// - 'next' will be called when it's OK to start the next method from
|
||||
// this client
|
||||
_run: function (args, callback, next) {
|
||||
var self = this;
|
||||
self._callback = callback;
|
||||
self._next = next;
|
||||
self._enclosing = Meteor._CurrentInvocation.get();
|
||||
self.isSimulation = !!(self._enclosing && self._enclosing.isSimulation);
|
||||
|
||||
try {
|
||||
var ret = Meteor._CurrentInvocation.withValue(self, function () {
|
||||
return self._handler.apply(self, args);
|
||||
});
|
||||
if (!self._responded && !self._async)
|
||||
self._sendResponse(undefined, ret, "sync");
|
||||
return ret;
|
||||
} catch (e) {
|
||||
// send response in "throw" mode, which will lock out any other
|
||||
// fibers kicked off by this method from emitting a response.
|
||||
self._sendResponse({error: 500, reason: "Internal server error"},
|
||||
undefined, "throw");
|
||||
// XXX improve error message (and how we report it)
|
||||
if (!e.expected)
|
||||
// tests can set the 'expected' flag on an exception so it
|
||||
// won't go to the server log
|
||||
Meteor._debug("Exception while invoking method '" +
|
||||
self._name + "'", e.stack);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
/* LivedataSession */
|
||||
/******************************************************************************/
|
||||
@@ -266,48 +130,57 @@ _.extend(Meteor._LivedataSession.prototype, {
|
||||
self.send(msg);
|
||||
},
|
||||
|
||||
// Throw 'msg' into the queue to be processed as an incoming
|
||||
// message, but ignore it if 'socket' is not the currently connected
|
||||
// socket.
|
||||
processMessage: function (msg, socket) {
|
||||
var self = this;
|
||||
if (socket === self.socket) {
|
||||
self.in_queue.push(msg);
|
||||
self._tryProcessNext();
|
||||
}
|
||||
},
|
||||
|
||||
// Process 'msg' as an incoming message. (But as a guard against
|
||||
// race conditions during reconnection, ignore the message if
|
||||
// 'socket' is not the currently connected socket.)
|
||||
//
|
||||
// We run the messages from the client one at a time, in the order
|
||||
// given by the client. A message can set self.blocked to pause
|
||||
// processing (eg, for an async method to complete.) To resume
|
||||
// processing, clear self.blocked and call _tryProcessNext().
|
||||
|
||||
// given by the client. The message handler is passed an idempotent
|
||||
// function 'unblock' which it may call to allow other messages to
|
||||
// begin running in parallel in another fiber (for example, a method
|
||||
// that wants to yield.) Otherwise, it is automatically unblocked
|
||||
// when it returns.
|
||||
//
|
||||
// Actually, we don't have to 'totally order' the messages in this
|
||||
// way, but it's the easiest thing that's correct. (unsub needs to
|
||||
// be ordered against sub, methods need to be ordered against each
|
||||
// other.)
|
||||
_tryProcessNext: function () {
|
||||
processMessage: function (msg_in, socket) {
|
||||
var self = this;
|
||||
|
||||
if (self.blocked || !self.in_queue.length || self.worker_running)
|
||||
if (socket !== self.socket)
|
||||
return;
|
||||
|
||||
self.in_queue.push(msg_in);
|
||||
if (self.worker_running)
|
||||
return;
|
||||
self.worker_running = true;
|
||||
Fiber(function () {
|
||||
while (true) {
|
||||
var more = !self.blocked && self.in_queue.length;
|
||||
if (!more) {
|
||||
self.worker_running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var msg = self.in_queue.shift();
|
||||
var processNext = function () {
|
||||
var msg = self.in_queue.shift();
|
||||
if (!msg) {
|
||||
self.worker_running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Fiber(function () {
|
||||
var blocked = true;
|
||||
|
||||
var unblock = function () {
|
||||
if (!blocked)
|
||||
return; // idempotent
|
||||
blocked = false;
|
||||
processNext();
|
||||
};
|
||||
|
||||
if (msg.msg in self.protocol_handlers)
|
||||
self.protocol_handlers[msg.msg].call(self, msg);
|
||||
self.protocol_handlers[msg.msg].call(self, msg, unblock);
|
||||
else
|
||||
self.sendError('Bad request', msg);
|
||||
}
|
||||
}).run();
|
||||
unblock(); // in case the handler didn't already do it
|
||||
}).run();
|
||||
};
|
||||
|
||||
processNext();
|
||||
},
|
||||
|
||||
protocol_handlers: {
|
||||
@@ -347,7 +220,7 @@ _.extend(Meteor._LivedataSession.prototype, {
|
||||
self.send({msg: 'nosub', id: msg.id});
|
||||
},
|
||||
|
||||
method: function (msg) {
|
||||
method: function (msg, unblock) {
|
||||
var self = this;
|
||||
|
||||
// reject malformed messages
|
||||
@@ -390,42 +263,39 @@ _.extend(Meteor._LivedataSession.prototype, {
|
||||
return;
|
||||
}
|
||||
|
||||
// Invocations sent by a given client block later invocations
|
||||
// sent by the same client until they return, unless the method
|
||||
// explicitly allows later methods to run by calling
|
||||
// beginAsync(true).
|
||||
|
||||
// XXX for completeness, should probably have a way of sending a
|
||||
// response *without* unblocking processing (and doing that
|
||||
// later..)
|
||||
|
||||
self.blocked = true;
|
||||
|
||||
var callback = function (error, result) {
|
||||
var payload = error ? {error: error} : {result: result};
|
||||
|
||||
self.result_cache[msg.id] =
|
||||
_.extend({when: +(new Date)}, payload);
|
||||
|
||||
self.send(
|
||||
_.extend({msg: 'result', id: msg.id}, payload));
|
||||
fence.arm();
|
||||
};
|
||||
|
||||
var next = function (error, ret) {
|
||||
self.blocked = false;
|
||||
self._tryProcessNext()
|
||||
};
|
||||
|
||||
var invocation = new Meteor._ServerMethodInvocation(msg.method, handler);
|
||||
var invocation = new Meteor._MethodInvocation(false /* is_simulation */,
|
||||
unblock);
|
||||
try {
|
||||
Meteor._CurrentWriteFence.withValue(fence, function () {
|
||||
invocation._run(msg.params || [], callback, next);
|
||||
});
|
||||
var ret =
|
||||
Meteor._CurrentWriteFence.withValue(fence, function () {
|
||||
return Meteor._CurrentInvocation.withValue(invocation, function () {
|
||||
return handler.apply(invocation, msg.params || []);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
// _run will have already logged the exception (and told the
|
||||
// client, if appropriate)
|
||||
var exception = e;
|
||||
}
|
||||
|
||||
fence.arm(); // we're done adding writes to the fence
|
||||
unblock(); // unblock, if the method hasn't done it already
|
||||
|
||||
// "blind" exceptions other than those that were deliberately
|
||||
// thrown to signal errors to the client
|
||||
if (exception && !(exception instanceof Meteor.Error)) {
|
||||
// tests can set the 'expected' flag on an exception so it
|
||||
// won't go to the server log
|
||||
if (!exception.expected)
|
||||
Meteor._debug("Exception while invoking method '" +
|
||||
msg.method + "'", exception.stack);
|
||||
exception = new Meteor.Error(500, "Internal server error");
|
||||
}
|
||||
|
||||
// send response and add to cache
|
||||
var payload =
|
||||
exception ? {error: exception} : (ret !== undefined ?
|
||||
{result: ret} : {});
|
||||
self.result_cache[msg.id] = _.extend({when: +(new Date)}, payload);
|
||||
self.send(_.extend({msg: 'result', id: msg.id}, payload));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -874,23 +744,38 @@ _.extend(Meteor._LivedataServer.prototype, {
|
||||
var self = this;
|
||||
|
||||
if (callback)
|
||||
// It's not really necessary to do this, since we immediately
|
||||
// run the callback in this fiber before returning, but we do it
|
||||
// anyway for regularity.
|
||||
callback = Meteor.bindEnvironment(callback, function (e) {
|
||||
// XXX improve error message (and how we report it)
|
||||
Meteor._debug("Exception while delivering result of invoking '" +
|
||||
name + "'", e.stack);
|
||||
});
|
||||
else
|
||||
callback = function () {};
|
||||
|
||||
// Run the handler
|
||||
var handler = self.method_handlers[name];
|
||||
if (!handler) {
|
||||
if (callback)
|
||||
callback({error: 404, reason: "Method not found"});
|
||||
throw new Error("No such method '" + name + "'");
|
||||
if (!handler)
|
||||
var exception = new Meteor.Error(404, "Method not found");
|
||||
else {
|
||||
var invocation = new Meteor._MethodInvocation(false /* is_simulation */);
|
||||
try {
|
||||
var ret = Meteor._CurrentInvocation.withValue(invocation, function () {
|
||||
return handler.apply(invocation, args)
|
||||
});
|
||||
} catch (e) {
|
||||
var exception = e;
|
||||
}
|
||||
}
|
||||
|
||||
var invocation = new Meteor._ServerMethodInvocation(name, handler);
|
||||
return invocation._run(args, callback);
|
||||
// Return the result in whichever way the caller asked for it
|
||||
if (callback) {
|
||||
callback(exception, ret);
|
||||
return;
|
||||
}
|
||||
if (exception)
|
||||
throw exception;
|
||||
return ret;
|
||||
},
|
||||
|
||||
// A much more elegant way to do this would be: let any autopublish
|
||||
@@ -910,12 +795,5 @@ _.extend(Meteor._LivedataServer.prototype, {
|
||||
self.on_autopublish.push(f);
|
||||
else
|
||||
f();
|
||||
},
|
||||
|
||||
// called when we are up-to-date. intended for use only in tests.
|
||||
onQuiesce: function (f) {
|
||||
var self = this;
|
||||
// the server is always up-to-date
|
||||
f();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
App.methods({
|
||||
Meteor.methods({
|
||||
nothing: function () {
|
||||
},
|
||||
echo: function (/* arguments */) {
|
||||
return _.toArray(arguments);
|
||||
},
|
||||
exception: function (where) {
|
||||
exception: function (where, intended) {
|
||||
var shouldThrow =
|
||||
(Meteor.is_server && where === "server") ||
|
||||
(Meteor.is_client && where === "client") ||
|
||||
where === "both";
|
||||
|
||||
if (shouldThrow) {
|
||||
e = new Error("Test method throwing an exception");
|
||||
if (intended)
|
||||
e = new Meteor.Error(999, "Client-visible test exception");
|
||||
else
|
||||
e = new Error("Test method throwing an exception");
|
||||
e.expected = true;
|
||||
throw e;
|
||||
}
|
||||
@@ -31,7 +36,7 @@ if (Meteor.is_server)
|
||||
world: world}});
|
||||
});
|
||||
|
||||
App.methods({
|
||||
Meteor.methods({
|
||||
'ledger/transfer': function (world, from_name, to_name, amount, cheat) {
|
||||
var from = Ledger.findOne({name: from_name, world: world});
|
||||
var to = Ledger.findOne({name: to_name, world: world});
|
||||
@@ -39,20 +44,16 @@ App.methods({
|
||||
if (Meteor.is_server)
|
||||
cheat = false;
|
||||
|
||||
if (!from) {
|
||||
this.error(404, "No such account " + from_name + " in " + world);
|
||||
return;
|
||||
}
|
||||
if (!from)
|
||||
throw new Meteor.Error(404,
|
||||
"No such account " + from_name + " in " + world);
|
||||
|
||||
if (!to) {
|
||||
this.error(404, "No such account " + to_name + " in " + world);
|
||||
return;
|
||||
}
|
||||
if (!to)
|
||||
throw new Meteor.Error(404,
|
||||
"No such account " + to_name + " in " + world);
|
||||
|
||||
if (from.balance < amount && !cheat) {
|
||||
this.error(409, "Insufficient funds");
|
||||
return;
|
||||
}
|
||||
if (from.balance < amount && !cheat)
|
||||
throw new Meteor.Error(409, "Insufficient funds");
|
||||
|
||||
Ledger.update({_id: from._id}, {$inc: {balance: -amount}});
|
||||
Ledger.update({_id: to._id}, {$inc: {balance: amount}});
|
||||
|
||||
@@ -1,189 +1,186 @@
|
||||
// XXX should probably move this into a testing helpers package so it
|
||||
// can be used by other tests
|
||||
|
||||
var ExpectationManager = function (test, onComplete) {
|
||||
var self = this;
|
||||
|
||||
self.test = test;
|
||||
self.onComplete = onComplete;
|
||||
self.closed = false;
|
||||
self.dead = false;
|
||||
self.outstanding = 0;
|
||||
};
|
||||
|
||||
_.extend(ExpectationManager.prototype, {
|
||||
expect: function (/* arguments */) {
|
||||
var self = this;
|
||||
|
||||
if (typeof arguments[0] === "function")
|
||||
var expected = arguments[0];
|
||||
else
|
||||
var expected = _.toArray(arguments);
|
||||
|
||||
if (self.closed)
|
||||
throw new Error("Too late to add more expectations to the test");
|
||||
self.outstanding++;
|
||||
|
||||
return function (/* arguments */) {
|
||||
if (typeof expected === "function")
|
||||
expected.apply({}, arguments);
|
||||
else
|
||||
self.test.equal(_.toArray(arguments), expected);
|
||||
|
||||
self.outstanding--;
|
||||
self._check_complete();
|
||||
};
|
||||
},
|
||||
|
||||
done: function () {
|
||||
var self = this;
|
||||
self.closed = true;
|
||||
self._check_complete();
|
||||
},
|
||||
|
||||
cancel: function () {
|
||||
var self = this;
|
||||
self.dead = true;
|
||||
},
|
||||
|
||||
_check_complete: function () {
|
||||
var self = this;
|
||||
if (!self.outstanding && self.closed && !self.dead) {
|
||||
self.dead = true;
|
||||
self.onComplete();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var testAsyncMulti = function (name, funcs) {
|
||||
var timeout = 5000;
|
||||
|
||||
Tinytest.addAsync(name, function (test, onComplete) {
|
||||
var remaining = _.clone(funcs);
|
||||
|
||||
var runNext = function () {
|
||||
var func = remaining.shift();
|
||||
if (!func)
|
||||
onComplete();
|
||||
else {
|
||||
var em = new ExpectationManager(test, function () {
|
||||
Meteor.clearTimeout(timer);
|
||||
runNext();
|
||||
});
|
||||
|
||||
var timer = Meteor.setTimeout(function () {
|
||||
em.cancel();
|
||||
test.fail({type: "timeout", message: "Async batch timed out"});
|
||||
onComplete();
|
||||
return;
|
||||
}, timeout);
|
||||
|
||||
try {
|
||||
func(test, _.bind(em.expect, em));
|
||||
} catch (exception) {
|
||||
em.cancel();
|
||||
test.exception(exception);
|
||||
Meteor.clearTimeout(timer);
|
||||
onComplete();
|
||||
return;
|
||||
}
|
||||
em.done();
|
||||
}
|
||||
};
|
||||
|
||||
runNext();
|
||||
});
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// XXX should check error codes
|
||||
var failure = function (test, code, reason) {
|
||||
return function (error, result) {
|
||||
test.equal(result, undefined);
|
||||
test.isTrue(error && typeof error === "object");
|
||||
if (error && typeof error === "object") {
|
||||
code && test.equal(error.error, code);
|
||||
reason && test.equal(error.reason, reason);
|
||||
// XXX should check that other keys aren't present.. should
|
||||
// probably use something like the Matcher we used to have
|
||||
if (typeof code === "number") {
|
||||
test.instanceOf(error, Meteor.Error);
|
||||
code && test.equal(error.error, code);
|
||||
reason && test.equal(error.reason, reason);
|
||||
// XXX should check that other keys aren't present.. should
|
||||
// probably use something like the Matcher we used to have
|
||||
} else {
|
||||
// for normal Javascript errors
|
||||
test.instanceOf(error, Error);
|
||||
test.equal(error.message, code);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Tinytest.add("livedata - Meteor.Error", function (test) {
|
||||
var error = new Meteor.Error(123, "kittens", "puppies");
|
||||
test.instanceOf(error, Error);
|
||||
test.equal(error.error, 123);
|
||||
test.equal(error.reason, "kittens");
|
||||
test.equal(error.details, "puppies");
|
||||
});
|
||||
|
||||
Tinytest.add("livedata - methods with colliding names", function (test) {
|
||||
var x = LocalCollection.uuid();
|
||||
var m = {};
|
||||
m[x] = function () {};
|
||||
App.methods(m);
|
||||
Meteor.methods(m);
|
||||
|
||||
test.throws(function () {
|
||||
App.methods(m);
|
||||
Meteor.methods(m);
|
||||
});
|
||||
});
|
||||
|
||||
testAsyncMulti("livedata - basic method invocation", [
|
||||
// Unknown methods
|
||||
function (test, expect) {
|
||||
try {
|
||||
var ret = App.call("unknown method",
|
||||
expect(failure(test, 404, "Method not found")));
|
||||
} catch (e) {
|
||||
// throws immediately on server, but still calls callback
|
||||
test.isTrue(Meteor.is_server);
|
||||
return;
|
||||
}
|
||||
|
||||
// returns undefined on client, then calls callback
|
||||
test.isTrue(Meteor.is_client);
|
||||
test.equal(ret, undefined);
|
||||
},
|
||||
|
||||
function (test, expect) {
|
||||
var ret = App.call("echo", expect(undefined, []));
|
||||
test.equal(ret, []);
|
||||
},
|
||||
|
||||
function (test, expect) {
|
||||
var ret = App.call("echo", 12, expect(undefined, [12]));
|
||||
test.equal(ret, [12]);
|
||||
},
|
||||
|
||||
function (test, expect) {
|
||||
var ret = App.call("echo", 12, {x: 13}, expect(undefined, [12, {x: 13}]));
|
||||
test.equal(ret, [12, {x: 13}]);
|
||||
},
|
||||
|
||||
function (test, expect) {
|
||||
test.throws(function () {
|
||||
var ret = App.call("exception", "both",
|
||||
expect(failure(test, 500, "Internal server error")));
|
||||
});
|
||||
},
|
||||
|
||||
function (test, expect) {
|
||||
try {
|
||||
var ret = App.call("exception", "server",
|
||||
expect(failure(test, 500, "Internal server error")));
|
||||
} catch (e) {
|
||||
test.isTrue(Meteor.is_server);
|
||||
return;
|
||||
}
|
||||
|
||||
test.isTrue(Meteor.is_client);
|
||||
test.equal(ret, undefined);
|
||||
},
|
||||
|
||||
function (test, expect) {
|
||||
if (Meteor.is_client) {
|
||||
test.throws(function () {
|
||||
var ret = App.call("exception", "client", expect(undefined, undefined));
|
||||
});
|
||||
} else {
|
||||
var ret = App.call("exception", "client", expect(undefined, undefined));
|
||||
if (Meteor.is_server) {
|
||||
// On server, with no callback, throws exception
|
||||
try {
|
||||
var ret = Meteor.call("unknown method");
|
||||
} catch (e) {
|
||||
test.equal(e.error, 404);
|
||||
var threw = true;
|
||||
}
|
||||
test.isTrue(threw);
|
||||
test.equal(ret, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
if (Meteor.is_client) {
|
||||
// On client, with no callback, just returns undefined
|
||||
var ret = Meteor.call("unknown method");
|
||||
test.equal(ret, undefined);
|
||||
}
|
||||
|
||||
// On either, with a callback, calls the callback and does not throw
|
||||
var ret = Meteor.call("unknown method",
|
||||
expect(failure(test, 404, "Method not found")));
|
||||
test.equal(ret, undefined);
|
||||
},
|
||||
|
||||
function (test, expect) {
|
||||
// make sure 'undefined' is preserved as such, instead of turning
|
||||
// into null (JSON does not have 'undefined' so there is special
|
||||
// code for this)
|
||||
if (Meteor.is_server)
|
||||
test.equal(Meteor.call("nothing"), undefined);
|
||||
if (Meteor.is_client)
|
||||
test.equal(Meteor.call("nothing"), undefined);
|
||||
|
||||
test.equal(Meteor.call("nothing", expect(undefined, undefined)), undefined);
|
||||
},
|
||||
|
||||
function (test, expect) {
|
||||
if (Meteor.is_server)
|
||||
test.equal(Meteor.call("echo"), []);
|
||||
if (Meteor.is_client)
|
||||
test.equal(Meteor.call("echo"), undefined);
|
||||
|
||||
test.equal(Meteor.call("echo", expect(undefined, [])), undefined);
|
||||
},
|
||||
|
||||
function (test, expect) {
|
||||
if (Meteor.is_server)
|
||||
test.equal(Meteor.call("echo", 12), [12]);
|
||||
if (Meteor.is_client)
|
||||
test.equal(Meteor.call("echo", 12), undefined);
|
||||
|
||||
test.equal(Meteor.call("echo", 12, expect(undefined, [12])), undefined);
|
||||
},
|
||||
|
||||
function (test, expect) {
|
||||
if (Meteor.is_server)
|
||||
test.equal(Meteor.call("echo", 12, {x: 13}), [12, {x: 13}]);
|
||||
if (Meteor.is_client)
|
||||
test.equal(Meteor.call("echo", 12, {x: 13}), undefined);
|
||||
|
||||
test.equal(Meteor.call("echo", 12, {x: 13},
|
||||
expect(undefined, [12, {x: 13}])), undefined);
|
||||
},
|
||||
|
||||
function (test, expect) {
|
||||
// No callback
|
||||
|
||||
if (Meteor.is_server) {
|
||||
test.throws(function () {
|
||||
Meteor.call("exception", "both");
|
||||
});
|
||||
test.throws(function () {
|
||||
Meteor.call("exception", "server");
|
||||
});
|
||||
// No exception, because no code will run on the client
|
||||
test.equal(Meteor.call("exception", "client"), undefined);
|
||||
}
|
||||
|
||||
if (Meteor.is_client) {
|
||||
// The client exception is thrown away because it's in the
|
||||
// stub. The server exception is throw away because we didn't
|
||||
// give a callback.
|
||||
test.equal(Meteor.call("exception", "both"), undefined);
|
||||
test.equal(Meteor.call("exception", "server"), undefined);
|
||||
test.equal(Meteor.call("exception", "client"), undefined);
|
||||
}
|
||||
|
||||
// With callback
|
||||
|
||||
if (Meteor.is_client) {
|
||||
test.equal(
|
||||
Meteor.call("exception", "both",
|
||||
expect(failure(test, 500, "Internal server error"))),
|
||||
undefined);
|
||||
test.equal(
|
||||
Meteor.call("exception", "server",
|
||||
expect(failure(test, 500, "Internal server error"))),
|
||||
undefined);
|
||||
test.equal(Meteor.call("exception", "client"), undefined);
|
||||
}
|
||||
|
||||
if (Meteor.is_server) {
|
||||
test.equal(
|
||||
Meteor.call("exception", "both",
|
||||
expect(failure(test, "Test method throwing an exception"))),
|
||||
undefined);
|
||||
test.equal(
|
||||
Meteor.call("exception", "server",
|
||||
expect(failure(test, "Test method throwing an exception"))),
|
||||
undefined);
|
||||
test.equal(Meteor.call("exception", "client"), undefined);
|
||||
}
|
||||
},
|
||||
|
||||
function (test, expect) {
|
||||
if (Meteor.is_server) {
|
||||
var threw = false;
|
||||
try {
|
||||
Meteor.call("exception", "both", true);
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
test.equal(e.error, 999);
|
||||
test.equal(e.reason, "Client-visible test exception");
|
||||
}
|
||||
test.isTrue(threw);
|
||||
}
|
||||
|
||||
if (Meteor.is_client) {
|
||||
test.equal(
|
||||
Meteor.call("exception", "both", true,
|
||||
expect(failure(test, 999,
|
||||
"Client-visible test exception"))),
|
||||
undefined);
|
||||
test.equal(
|
||||
Meteor.call("exception", "server", true,
|
||||
expect(failure(test, 999,
|
||||
"Client-visible test exception"))),
|
||||
undefined);
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
@@ -194,6 +191,13 @@ var checkBalances = function (test, a, b) {
|
||||
test.equal(bob.balance, b);
|
||||
}
|
||||
|
||||
var onQuiesce = function (f) {
|
||||
if (Meteor.is_server)
|
||||
f();
|
||||
else
|
||||
Meteor.default_connection.onQuiesce(f);
|
||||
};
|
||||
|
||||
// would be nice to have a database-aware test harness of some kind --
|
||||
// this is a big hack (and XXX pollutes the global test namespace)
|
||||
testAsyncMulti("livedata - compound methods", [
|
||||
@@ -204,20 +208,20 @@ testAsyncMulti("livedata - compound methods", [
|
||||
Ledger.insert({name: "bob", balance: 50, world: test.runId()});
|
||||
},
|
||||
function (test, expect) {
|
||||
App.call('ledger/transfer', test.runId(), "alice", "bob", 10,
|
||||
expect(undefined, undefined));
|
||||
Meteor.call('ledger/transfer', test.runId(), "alice", "bob", 10,
|
||||
expect(undefined, undefined));
|
||||
|
||||
checkBalances(test, 90, 60);
|
||||
|
||||
var release = expect();
|
||||
App.onQuiesce(function () {
|
||||
onQuiesce(function () {
|
||||
checkBalances(test, 90, 60);
|
||||
Meteor.defer(release);
|
||||
});
|
||||
},
|
||||
function (test, expect) {
|
||||
App.call('ledger/transfer', test.runId(), "alice", "bob", 100, true,
|
||||
expect(failure(test, 409)));
|
||||
Meteor.call('ledger/transfer', test.runId(), "alice", "bob", 100, true,
|
||||
expect(failure(test, 409)));
|
||||
|
||||
if (Meteor.is_client)
|
||||
// client can fool itself by cheating, but only until the sync
|
||||
@@ -227,7 +231,7 @@ testAsyncMulti("livedata - compound methods", [
|
||||
checkBalances(test, 90, 60);
|
||||
|
||||
var release = expect();
|
||||
App.onQuiesce(function () {
|
||||
onQuiesce(function () {
|
||||
checkBalances(test, 90, 60);
|
||||
Meteor.defer(release);
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ Package.on_use(function (api) {
|
||||
Package.on_test(function (api) {
|
||||
api.use('livedata', ['client', 'server']);
|
||||
api.use('mongo-livedata', ['client', 'server']);
|
||||
api.use('test-helpers', ['client', 'server']);
|
||||
api.use('tinytest');
|
||||
api.add_files('livedata_tests.js', ['client', 'server']);
|
||||
api.add_files('livedata_test_service.js', ['client', 'server']);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
App = new Meteor._LivedataServer;
|
||||
|
||||
_.extend(Meteor, {
|
||||
publish: _.bind(App.publish, App),
|
||||
default_server: new Meteor._LivedataServer,
|
||||
|
||||
refresh: function (notification) {
|
||||
var fence = Meteor._CurrentWriteFence.get();
|
||||
@@ -16,3 +14,11 @@ _.extend(Meteor, {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Proxy the public methods of Meteor.default_server so they can
|
||||
// be called directly on Meteor.
|
||||
_.each(['publish', 'methods', 'call', 'apply'],
|
||||
function (name) {
|
||||
Meteor[name] = _.bind(Meteor.default_server[name],
|
||||
Meteor.default_server);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
(function() {
|
||||
var suppress = 0;
|
||||
|
||||
// replacement for console.log. This is a temporary API. We should
|
||||
// provide a real logging API soon (possibly just a polyfill for
|
||||
// console?)
|
||||
@@ -10,6 +12,10 @@
|
||||
// be very visible. if you change _debug to go someplace else, etc,
|
||||
// please fix the autopublish code to do something reasonable.
|
||||
Meteor._debug = function (/* arguments */) {
|
||||
if (suppress) {
|
||||
suppress--;
|
||||
return;
|
||||
}
|
||||
if (typeof console !== 'undefined' &&
|
||||
typeof console.log !== 'undefined') {
|
||||
if (arguments.length == 0) { // IE Companion breaks otherwise
|
||||
@@ -20,4 +26,10 @@
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Suppress the next 'count' Meteor._debug messsages. Use this to
|
||||
// stop tests from spamming the console.
|
||||
Meteor._suppress_log = function (count) {
|
||||
suppress += count;
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
Tinytest.add("logging", function (test) {
|
||||
|
||||
// Just run a log statement and make sure it doesn't explode.
|
||||
Meteor._suppress_log(3);
|
||||
Meteor._debug();
|
||||
Meteor._debug("test one arg");
|
||||
Meteor._debug("this", "is", "a", "test");
|
||||
|
||||
@@ -10,10 +10,13 @@ Meteor.Collection = function (name, manager, driver) {
|
||||
}
|
||||
|
||||
// note: nameless collections never have a manager
|
||||
manager = name && (manager || App);
|
||||
manager = name && (manager ||
|
||||
(Meteor.is_client ?
|
||||
Meteor.default_connection : Meteor.default_server));
|
||||
|
||||
if (!driver) {
|
||||
if (name && manager === App && Meteor._RemoteCollectionDriver)
|
||||
if (name && manager === Meteor.default_server &&
|
||||
Meteor._RemoteCollectionDriver)
|
||||
driver = Meteor._RemoteCollectionDriver;
|
||||
else
|
||||
driver = Meteor._LocalCollectionDriver;
|
||||
@@ -85,17 +88,20 @@ Meteor.Collection = function (name, manager, driver) {
|
||||
self._prefix = '/' + name + '/';
|
||||
m[self._prefix + 'insert'] = function (/* selector, options */) {
|
||||
self._maybe_snapshot();
|
||||
return self._collection.insert.apply(self._collection, _.toArray(arguments));
|
||||
// Allow exceptions to propagate
|
||||
self._collection.insert.apply(self._collection, _.toArray(arguments));
|
||||
};
|
||||
|
||||
m[self._prefix + 'update'] = function (/* selector, mutator, options */) {
|
||||
self._maybe_snapshot();
|
||||
return self._collection.update.apply(self._collection, _.toArray(arguments));
|
||||
// Allow exceptions to propagate
|
||||
self._collection.update.apply(self._collection, _.toArray(arguments));
|
||||
};
|
||||
|
||||
m[self._prefix + 'remove'] = function (/* selector */) {
|
||||
self._maybe_snapshot();
|
||||
return self._collection.remove.apply(self._collection, _.toArray(arguments));
|
||||
// Allow exceptions to propagate
|
||||
self._collection.remove.apply(self._collection, _.toArray(arguments));
|
||||
};
|
||||
|
||||
manager.methods(m);
|
||||
@@ -129,43 +135,84 @@ _.extend(Meteor.Collection.prototype, {
|
||||
self._collection.snapshot();
|
||||
self._was_snapshot = true;
|
||||
}
|
||||
},
|
||||
|
||||
// XXX provide a way for the caller to find out about errors from
|
||||
// the server? probably the answer is: detect a function at the end
|
||||
// of the arguments, use as a callback ... same semantics as methods
|
||||
// usually have?
|
||||
|
||||
insert: function (doc) {
|
||||
var self = this;
|
||||
|
||||
// shallow-copy the document and generate an ID
|
||||
doc = _.extend({}, doc);
|
||||
doc._id = Meteor.uuid();
|
||||
|
||||
if (self._manager)
|
||||
self._manager.call(self._prefix + 'insert', doc);
|
||||
else
|
||||
self._collection.insert(doc);
|
||||
|
||||
return doc;
|
||||
},
|
||||
|
||||
update: function (/* arguments */) {
|
||||
var self = this;
|
||||
|
||||
if (self._manager)
|
||||
self._manager.apply(self._prefix + 'update', _.toArray(arguments));
|
||||
else
|
||||
self._collection.update.apply(self._collection, _.toArray(arguments));
|
||||
},
|
||||
|
||||
remove: function (/* arguments */) {
|
||||
var self = this;
|
||||
|
||||
if (self._manager)
|
||||
self._manager.apply(self._prefix + 'remove', _.toArray(arguments));
|
||||
else
|
||||
self._collection.remove.apply(self._collection, _.toArray(arguments));
|
||||
}
|
||||
});
|
||||
|
||||
// 'insert' immediately returns a copy of the inserted document with
|
||||
// the _id added. The others return nothing.
|
||||
//
|
||||
// Otherwise, the semantics are exactly like other methods: they take
|
||||
// a callback as an optional last argument; if no callback is
|
||||
// provided, they block until the operation is complete, and throw an
|
||||
// exception if it fails; if a callback is provided, then they don't
|
||||
// necessarily block, and they call the callback when they finish with
|
||||
// zero arguments on success, or one argument, an exception, on
|
||||
// failure; on the client, blocking is impossible, so if a callback
|
||||
// isn't provided, they just return immediately and any error
|
||||
// information is lost.
|
||||
//
|
||||
// There's one more tweak. On the client, if you don't provide a
|
||||
// callback, then if there is an error, a message will be logged with
|
||||
// Meteor._debug.
|
||||
//
|
||||
// The intent (though this is actually determined by the underlying
|
||||
// drivers) is that the operations should be done synchronously, not
|
||||
// generating their result until the database has acknowledged
|
||||
// them. In the future maybe we should provide a flag to turn this
|
||||
// off.
|
||||
_.each(["insert", "update", "remove"], function (name) {
|
||||
Meteor.Collection.prototype[name] = function (/* arguments */) {
|
||||
var self = this;
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
if (args.length && args[args.length - 1] instanceof Function)
|
||||
var callback = args.pop();
|
||||
|
||||
if (Meteor.is_client && !callback)
|
||||
// Client can't block, so it can't report errors by exception,
|
||||
// only by callback. If they forget the callback, give them a
|
||||
// default one that logs the error, so they aren't totally
|
||||
// baffled if their writes don't work because their database is
|
||||
// down.
|
||||
callback = function (err) {
|
||||
if (err)
|
||||
Meteor._debug(name + " failed: " + err.error + " -- " + err.reason);
|
||||
};
|
||||
|
||||
if (name === "insert") {
|
||||
if (!args.length)
|
||||
throw new Error("insert requires an argument");
|
||||
// shallow-copy the document and generate an ID
|
||||
args[0] = _.extend({}, args[0]);
|
||||
if ('_id' in args[0])
|
||||
throw new Error("Do not pass an _id to insert. Meteor will generate the _id for you.");
|
||||
var ret = args[0]._id = Meteor.uuid();
|
||||
}
|
||||
|
||||
if (self._manager) {
|
||||
// NB: on failure, allow exception to propagate
|
||||
self._manager.apply(self._prefix + name, args, callback);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
self._collection[name].apply(self._collection, args);
|
||||
} catch (e) {
|
||||
if (callback) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Note that on the client, this will never happen, because
|
||||
// we will have been provided with a default callback. (This
|
||||
// is nice because it matches the behavior of named
|
||||
// collections, which on the client never throw exceptions
|
||||
// directly.)
|
||||
throw e;
|
||||
}
|
||||
|
||||
callback && callback();
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
});
|
||||
|
||||
@@ -74,6 +74,13 @@ _Mongo.prototype._maybeBeginWrite = function () {
|
||||
|
||||
//////////// Public API //////////
|
||||
|
||||
// The write methods block until the database has confirmed the write
|
||||
// (it may not be replicated or stable on disk, but one server has
|
||||
// confirmed it.) (In the future we might have an option to turn this
|
||||
// off, ie, to enqueue the request on the wire and return
|
||||
// immediately.) They return nothing on success, and raise an
|
||||
// exception on failure.
|
||||
//
|
||||
// After making a write (with insert, update, remove), observers are
|
||||
// notified asynchronously. If you want to receive a callback once all
|
||||
// of the observer notifications have landed for your write, do the
|
||||
@@ -86,74 +93,139 @@ _Mongo.prototype._maybeBeginWrite = function () {
|
||||
|
||||
_Mongo.prototype.insert = function (collection_name, document) {
|
||||
var self = this;
|
||||
|
||||
if (collection_name === "___meteor_failure_test_collection" &&
|
||||
document.fail) {
|
||||
var e = new Error("Failure test");
|
||||
e.expected = true;
|
||||
throw e;
|
||||
}
|
||||
|
||||
var write = self._maybeBeginWrite();
|
||||
|
||||
var future = new Future;
|
||||
self._withCollection(collection_name, function(err, collection) {
|
||||
// XXX err handling
|
||||
collection.insert(document, {/* safe: true */}, function(err) {
|
||||
// XXX err handling
|
||||
});
|
||||
|
||||
var finish = Meteor.bindEnvironment(function () {
|
||||
Meteor.refresh({collection: collection_name});
|
||||
write.committed();
|
||||
|
||||
future.ret();
|
||||
}, function (e) {
|
||||
Meteor._debug("Exception while completing insert: " + e.stack);
|
||||
});
|
||||
|
||||
return future.wait();
|
||||
var future = new Future;
|
||||
self._withCollection(collection_name, function (err, collection) {
|
||||
if (err) {
|
||||
future.ret(err);
|
||||
return;
|
||||
}
|
||||
|
||||
collection.insert(document, {safe: true}, function (err) {
|
||||
if (err) {
|
||||
future.ret(err);
|
||||
return;
|
||||
}
|
||||
|
||||
finish();
|
||||
future.ret();
|
||||
});
|
||||
});
|
||||
|
||||
var err = future.wait();
|
||||
if (err)
|
||||
throw err;
|
||||
};
|
||||
|
||||
_Mongo.prototype.remove = function (collection_name, selector) {
|
||||
var self = this;
|
||||
|
||||
if (collection_name === "___meteor_failure_test_collection" &&
|
||||
selector.fail) {
|
||||
var e = new Error("Failure test");
|
||||
e.expected = true;
|
||||
throw e;
|
||||
}
|
||||
|
||||
var write = self._maybeBeginWrite();
|
||||
|
||||
var finish = Meteor.bindEnvironment(function () {
|
||||
Meteor.refresh({collection: collection_name});
|
||||
write.committed();
|
||||
}, function (e) {
|
||||
Meteor._debug("Exception while completing remove: " + e.stack);
|
||||
});
|
||||
|
||||
// XXX does not allow options. matches the client.
|
||||
selector = _Mongo._rewriteSelector(selector);
|
||||
|
||||
var future = new Future;
|
||||
self._withCollection(collection_name, function(err, collection) {
|
||||
// XXX err handling
|
||||
collection.remove(selector, {/* safe: true */}, function(err) {
|
||||
// XXX err handling
|
||||
self._withCollection(collection_name, function (err, collection) {
|
||||
if (err) {
|
||||
future.ret(err);
|
||||
return;
|
||||
}
|
||||
|
||||
collection.remove(selector, {/* XXXsafe: true*/}, function (err) {
|
||||
if (err) {
|
||||
future.ret(err);
|
||||
return;
|
||||
}
|
||||
|
||||
finish();
|
||||
future.ret();
|
||||
});
|
||||
|
||||
Meteor.refresh({collection: collection_name});
|
||||
write.committed();
|
||||
|
||||
future.ret();
|
||||
});
|
||||
|
||||
return future.wait();
|
||||
var err = future.wait();
|
||||
if (err)
|
||||
throw err;
|
||||
};
|
||||
|
||||
_Mongo.prototype.update = function (collection_name, selector, mod, options) {
|
||||
var self = this;
|
||||
|
||||
if (collection_name === "___meteor_failure_test_collection" &&
|
||||
selector.fail) {
|
||||
var e = new Error("Failure test");
|
||||
e.expected = true;
|
||||
throw e;
|
||||
}
|
||||
|
||||
var write = self._maybeBeginWrite();
|
||||
|
||||
var finish = Meteor.bindEnvironment(function () {
|
||||
Meteor.refresh({collection: collection_name});
|
||||
write.committed();
|
||||
}, function (e) {
|
||||
Meteor._debug("Exception while completing update: " + e.stack);
|
||||
});
|
||||
|
||||
selector = _Mongo._rewriteSelector(selector);
|
||||
if (!options) options = {};
|
||||
|
||||
var future = new Future;
|
||||
self._withCollection(collection_name, function(err, collection) {
|
||||
// XXX err handling
|
||||
self._withCollection(collection_name, function (err, collection) {
|
||||
if (err) {
|
||||
future.ret(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var opts = {/* safe: true */};
|
||||
var opts = {safe: true};
|
||||
// explictly enumerate options that minimongo supports
|
||||
if (options.upsert) opts.upsert = true;
|
||||
if (options.multi) opts.multi = true;
|
||||
|
||||
collection.update(selector, mod, opts, function(err) {
|
||||
// XXX err handling
|
||||
collection.update(selector, mod, opts, function (err) {
|
||||
if (err) {
|
||||
future.ret(err);
|
||||
return;
|
||||
}
|
||||
|
||||
finish();
|
||||
future.ret();
|
||||
});
|
||||
|
||||
Meteor.refresh({collection: collection_name});
|
||||
write.committed();
|
||||
|
||||
future.ret();
|
||||
});
|
||||
|
||||
return future.wait();
|
||||
var err = future.wait();
|
||||
if (err)
|
||||
throw err;
|
||||
};
|
||||
|
||||
_Mongo.prototype.find = function (collection_name, selector, options) {
|
||||
@@ -162,7 +234,7 @@ _Mongo.prototype.find = function (collection_name, selector, options) {
|
||||
if (arguments.length === 1)
|
||||
selector = {};
|
||||
|
||||
return new _Mongo.Cursor(self, collection_name, selector, options);
|
||||
return _Mongo._makeCursor(self, collection_name, selector, options);
|
||||
};
|
||||
|
||||
_Mongo.prototype.findOne = function (collection_name, selector, options) {
|
||||
@@ -171,28 +243,52 @@ _Mongo.prototype.findOne = function (collection_name, selector, options) {
|
||||
if (arguments.length === 1)
|
||||
selector = {};
|
||||
|
||||
return this.find(collection_name, selector, options).fetch()[0];
|
||||
return self.find(collection_name, selector, options).fetch()[0];
|
||||
};
|
||||
|
||||
// Cursors
|
||||
|
||||
_Mongo.Cursor = function (mongo, collection_name, selector, options) {
|
||||
var self = this;
|
||||
|
||||
self.mongo = mongo;
|
||||
self.collection_name = collection_name;
|
||||
self.selector = _Mongo._rewriteSelector(selector);
|
||||
self.options = options || {};
|
||||
|
||||
// Returns a _Mongo.Cursor, or throws an exception on
|
||||
// failure. Creating a cursor involves a database query, and we block
|
||||
// until it returns.
|
||||
_Mongo._makeCursor = function (mongo, collection_name, selector, options) {
|
||||
var future = new Future;
|
||||
|
||||
self.mongo._withCollection(collection_name, function(err, collection) {
|
||||
// XXX err handling
|
||||
var cursor = collection.find(self.selector, self.options.fields, self.options.skip, self.options.limit, self.options.sort);
|
||||
future.ret(cursor);
|
||||
options = options || {};
|
||||
selector = _Mongo._rewriteSelector(selector);
|
||||
|
||||
mongo._withCollection(collection_name, function (err, collection) {
|
||||
if (err) {
|
||||
future.ret([false, err]);
|
||||
return
|
||||
}
|
||||
|
||||
var cursor = collection.find(selector, options.fields,
|
||||
options.skip, options.limit, options.sort);
|
||||
future.ret([true, cursor]);
|
||||
});
|
||||
|
||||
this.cursor = future.wait();
|
||||
var result = future.wait();
|
||||
if (!(result[0]))
|
||||
throw result[1];
|
||||
|
||||
return new _Mongo.Cursor(mongo, collection_name, selector, options,
|
||||
result[1]);
|
||||
};
|
||||
|
||||
// Do not call directly. Use _Mongo._makeCursor instead.
|
||||
_Mongo.Cursor = function (mongo, collection_name, selector, options, cursor) {
|
||||
var self = this;
|
||||
|
||||
if (!cursor)
|
||||
throw new Error("Cursor required");
|
||||
|
||||
// NB: 'options' and 'selector' have already been preprocessed by _makeCursor
|
||||
self.mongo = mongo;
|
||||
self.collection_name = collection_name;
|
||||
self.selector = selector;
|
||||
self.options = options;
|
||||
self.cursor = cursor;
|
||||
};
|
||||
|
||||
_Mongo.Cursor.prototype.forEach = function (callback) {
|
||||
@@ -205,7 +301,10 @@ _Mongo.Cursor.prototype.forEach = function (callback) {
|
||||
else
|
||||
callback(doc);
|
||||
});
|
||||
return future.wait();
|
||||
|
||||
var err = future.wait();
|
||||
if (err)
|
||||
throw err;
|
||||
};
|
||||
|
||||
_Mongo.Cursor.prototype.map = function (callback) {
|
||||
@@ -229,10 +328,13 @@ _Mongo.Cursor.prototype.fetch = function () {
|
||||
var future = new Future;
|
||||
|
||||
self.cursor.toArray(function (err, res) {
|
||||
future.ret(err || res);
|
||||
future.ret([err, res]);
|
||||
});
|
||||
|
||||
return future.wait();
|
||||
var result = future.wait();
|
||||
if (result[0])
|
||||
throw result[0];
|
||||
return result[1];
|
||||
};
|
||||
|
||||
_Mongo.Cursor.prototype.count = function () {
|
||||
@@ -240,10 +342,13 @@ _Mongo.Cursor.prototype.count = function () {
|
||||
var future = new Future;
|
||||
|
||||
self.cursor.count(function (err, res) {
|
||||
future.ret(err || res);
|
||||
future.ret([err, res]);
|
||||
});
|
||||
|
||||
return future.wait();
|
||||
var err = future.wait();
|
||||
if (result[0])
|
||||
throw result[0];
|
||||
return result[1];
|
||||
};
|
||||
|
||||
// options to contain:
|
||||
@@ -265,10 +370,10 @@ _Mongo.LiveResultsSet = function (cursor, options) {
|
||||
|
||||
// copy my cursor, so that the observe can run independently from
|
||||
// some other use of the cursor.
|
||||
self.cursor = new _Mongo.Cursor(cursor.mongo,
|
||||
cursor.collection_name,
|
||||
cursor.selector,
|
||||
cursor.options);
|
||||
self.cursor = _Mongo._makeCursor(cursor.mongo,
|
||||
cursor.collection_name,
|
||||
cursor.selector,
|
||||
cursor.options);
|
||||
|
||||
// expose collection name
|
||||
self.collection_name = cursor.collection_name;
|
||||
|
||||
34
packages/mongo-livedata/mongo_livedata_tests.js
Normal file
34
packages/mongo-livedata/mongo_livedata_tests.js
Normal file
@@ -0,0 +1,34 @@
|
||||
// This is a magic collection that fails its writes on the server when
|
||||
// the selector (or inserted document) contains fail: true.
|
||||
|
||||
// XXX namespacing
|
||||
Meteor._FailureTestCollection =
|
||||
new Meteor.Collection("___meteor_failure_test_collection");
|
||||
|
||||
testAsyncMulti("mongo-livedata - database failure reporting", [
|
||||
function (test, expect) {
|
||||
var ftc = Meteor._FailureTestCollection;
|
||||
|
||||
var exception = function (err) {
|
||||
test.instanceOf(err, Error);
|
||||
};
|
||||
|
||||
_.each(["insert", "remove", "update"], function (op) {
|
||||
if (Meteor.is_server) {
|
||||
test.throws(function () {
|
||||
ftc[op]({fail: true});
|
||||
});
|
||||
|
||||
ftc[op]({fail: true}, expect(exception));
|
||||
}
|
||||
|
||||
if (Meteor.is_client) {
|
||||
ftc[op]({fail: true}, expect(exception));
|
||||
|
||||
// This would log to console in normal operation.
|
||||
Meteor._suppress_log(1);
|
||||
ftc[op]({fail: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
]);
|
||||
@@ -21,3 +21,9 @@ Package.on_use(function (api) {
|
||||
api.add_files('remote_collection_driver.js', 'server');
|
||||
api.add_files('collection.js', ['client', 'server']);
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use('mongo-livedata');
|
||||
api.use('tinytest');
|
||||
api.add_files('mongo_livedata_tests.js', ['client', 'server']);
|
||||
});
|
||||
97
packages/test-helpers/async_multi.js
Normal file
97
packages/test-helpers/async_multi.js
Normal file
@@ -0,0 +1,97 @@
|
||||
// This depends on tinytest, so it's a little weird to put it in
|
||||
// test-helpers, but it'll do for now.
|
||||
|
||||
var ExpectationManager = function (test, onComplete) {
|
||||
var self = this;
|
||||
|
||||
self.test = test;
|
||||
self.onComplete = onComplete;
|
||||
self.closed = false;
|
||||
self.dead = false;
|
||||
self.outstanding = 0;
|
||||
};
|
||||
|
||||
_.extend(ExpectationManager.prototype, {
|
||||
expect: function (/* arguments */) {
|
||||
var self = this;
|
||||
|
||||
if (typeof arguments[0] === "function")
|
||||
var expected = arguments[0];
|
||||
else
|
||||
var expected = _.toArray(arguments);
|
||||
|
||||
if (self.closed)
|
||||
throw new Error("Too late to add more expectations to the test");
|
||||
self.outstanding++;
|
||||
|
||||
return function (/* arguments */) {
|
||||
if (typeof expected === "function")
|
||||
expected.apply({}, arguments);
|
||||
else
|
||||
self.test.equal(_.toArray(arguments), expected);
|
||||
|
||||
self.outstanding--;
|
||||
self._check_complete();
|
||||
};
|
||||
},
|
||||
|
||||
done: function () {
|
||||
var self = this;
|
||||
self.closed = true;
|
||||
self._check_complete();
|
||||
},
|
||||
|
||||
cancel: function () {
|
||||
var self = this;
|
||||
self.dead = true;
|
||||
},
|
||||
|
||||
_check_complete: function () {
|
||||
var self = this;
|
||||
if (!self.outstanding && self.closed && !self.dead) {
|
||||
self.dead = true;
|
||||
self.onComplete();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var testAsyncMulti = function (name, funcs) {
|
||||
var timeout = 5000;
|
||||
|
||||
Tinytest.addAsync(name, function (test, onComplete) {
|
||||
var remaining = _.clone(funcs);
|
||||
|
||||
var runNext = function () {
|
||||
var func = remaining.shift();
|
||||
if (!func)
|
||||
onComplete();
|
||||
else {
|
||||
var em = new ExpectationManager(test, function () {
|
||||
Meteor.clearTimeout(timer);
|
||||
runNext();
|
||||
});
|
||||
|
||||
var timer = Meteor.setTimeout(function () {
|
||||
em.cancel();
|
||||
test.fail({type: "timeout", message: "Async batch timed out"});
|
||||
onComplete();
|
||||
return;
|
||||
}, timeout);
|
||||
|
||||
try {
|
||||
func(test, _.bind(em.expect, em));
|
||||
} catch (exception) {
|
||||
em.cancel();
|
||||
test.exception(exception);
|
||||
Meteor.clearTimeout(timer);
|
||||
// Because we called test.exception, we're not to call onComplete.
|
||||
return;
|
||||
}
|
||||
em.done();
|
||||
}
|
||||
};
|
||||
|
||||
runNext();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ Package.on_use(function (api, where) {
|
||||
where = where || ["client", "server"];
|
||||
|
||||
api.add_files('try_all_permutations.js', where);
|
||||
api.add_files('async_multi.js', where);
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
|
||||
@@ -182,18 +182,39 @@ _.extend(TestCase.prototype, {
|
||||
// test raised (or voluntarily reported) an exception.
|
||||
run: function (onEvent, onComplete, onException, stop_at_offset) {
|
||||
var self = this;
|
||||
var results = new TestCaseResults(self, onEvent, onException,
|
||||
stop_at_offset);
|
||||
|
||||
var completed = false;
|
||||
var markComplete = function () {
|
||||
if (completed) {
|
||||
Meteor._debug("*** Test error -- test '" + self.name +
|
||||
"' returned multiple times.");
|
||||
return false;
|
||||
}
|
||||
completed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
var results = new TestCaseResults(self, onEvent,
|
||||
function (e) {
|
||||
if (markComplete())
|
||||
onException(e);
|
||||
}, stop_at_offset);
|
||||
|
||||
Meteor.defer(function () {
|
||||
try {
|
||||
if (self.async)
|
||||
self.func(results, onComplete);
|
||||
else {
|
||||
if (self.async) {
|
||||
self.func(results, function () {
|
||||
if (markComplete())
|
||||
onComplete()
|
||||
});
|
||||
} else {
|
||||
self.func(results);
|
||||
onComplete();
|
||||
if (markComplete())
|
||||
onComplete();
|
||||
}
|
||||
} catch (e) {
|
||||
onException(e);
|
||||
if (markComplete())
|
||||
onException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,13 +19,13 @@ Meteor._runTestsEverywhere = function (onReport, onComplete) {
|
||||
maybeDone();
|
||||
});
|
||||
|
||||
App.call('tinytest/run', run_id, function (error, result) {
|
||||
Meteor.call('tinytest/run', run_id, function (error, result) {
|
||||
if (error)
|
||||
// XXX better report error
|
||||
throw new Error("Test server returned an error");
|
||||
});
|
||||
|
||||
App.onQuiesce(function () {
|
||||
Meteor.default_connection.onQuiesce(function () {
|
||||
// XXX use _.defer to avoid calling into minimongo
|
||||
// reentrantly. we need to handle this better..
|
||||
// (XXX code got refactored -- still necessary?)
|
||||
@@ -46,7 +46,7 @@ Meteor._runTestsEverywhere = function (onReport, onComplete) {
|
||||
});
|
||||
});
|
||||
|
||||
var sub_handle = App.subscribe('tinytest/results', run_id);
|
||||
var sub_handle = Meteor.subscribe('tinytest/results', run_id);
|
||||
var query_handle = Meteor._ServerTestResults.find().observe({
|
||||
added: function (doc) {
|
||||
_.each(doc.report.events || [], function (event) {
|
||||
|
||||
@@ -2,16 +2,19 @@ Meteor.startup(function () {
|
||||
Meteor._ServerTestResults.remove();
|
||||
});
|
||||
|
||||
App.publish('tinytest/results', function (run_id) {
|
||||
Meteor.publish('tinytest/results', function (run_id) {
|
||||
return Meteor._ServerTestResults.find({run_id: run_id},
|
||||
{key: {collection: 'tinytest_results',
|
||||
run_id: run_id}});
|
||||
});
|
||||
|
||||
App.methods({
|
||||
Meteor.methods({
|
||||
'tinytest/run': function (run_id) {
|
||||
var request = this;
|
||||
request.beginAsync();
|
||||
this.unblock();
|
||||
|
||||
// XXX using private API === lame
|
||||
var Future = __meteor_bootstrap__.require('fibers/future');
|
||||
var future = new Future;
|
||||
|
||||
Meteor._runTests(function (report) {
|
||||
/* onReport */
|
||||
@@ -19,7 +22,9 @@ App.methods({
|
||||
Meteor.refresh({collection: 'tinytest_results', run_id: run_id});
|
||||
}, function () {
|
||||
/* onComplete */
|
||||
request.respond();
|
||||
future['return']();
|
||||
});
|
||||
|
||||
future.wait();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user