diff --git a/History.md b/History.md
index 3c86fddf58..3add885bb3 100644
--- a/History.md
+++ b/History.md
@@ -22,6 +22,9 @@
For more information on Meteor Accounts, see http://docs.meteor.com/#accounts
+* The new function `Meteor.autorun` allows you run any code in a reactive
+ context. See http://docs.meteor.com/#meteor_autorun
+
* Arrays and objects can now be stored in the `Session`; mutating the value you
retrieve with `Session.get` does not affect the value in the session.
diff --git a/docs/client/api.html b/docs/client/api.html
index 18fbd7fc59..c7d8245b90 100644
--- a/docs/client/api.html
+++ b/docs/client/api.html
@@ -2110,6 +2110,34 @@ might think of it as a dynamically scoped ("special") variable. (That
just means that [`run`](#run) sets it, runs some user-supplied code, and
then restores its previous value.)
+{{> api_box autorun }}
+
+`Meteor.autorun` allows you to set up your own reactive context, where you can
+perform arbitrary actions when dependencies change. For example, you can monitor
+a cursor (which is a reactive data source) and aggregate it into a session
+variable:
+
+ Meteor.autorun(function() {
+ var oldest = _.max(Monkeys.find().fetch(), function (monkey) {
+ return monkey.age;
+ });
+ if (oldest)
+ Session.set("oldest", oldest.name);
+ });
+
+Or you can wait for a session variable to get a certain value, and do something
+the first time it does so, using the `stop` handle to prevent further runs:
+
+ Meteor.autorun(function(handle) {
+ if (!Session.equals("shouldAlert", true)) return;
+ handle.stop();
+ alert("Oh no!");
+ });
+
+The function is invoked immediately and — like all reactive
+sources — the rerun occurs at the time of the next
+[`Meteor.flush`](#meteor_flush).
+
{{> api_box flush }}
@@ -2143,8 +2171,6 @@ elements are cleaned up by logic that is triggered by context invalidations.
-
-
Meteor.http
`Meteor.http` provides an HTTP API on the client and server. To use
diff --git a/docs/client/api.js b/docs/client/api.js
index e23c444d07..f5e7055f0a 100644
--- a/docs/client/api.js
+++ b/docs/client/api.js
@@ -608,6 +608,18 @@ Template.api.current = {
descr: ["The current [`invalidation context`](#context), or `null` if not being called from inside [`run`](#run)."]
};
+Template.api.autorun = {
+ id: "meteor_autorun",
+ name: "Meteor.autorun(func)",
+ locus: "Client",
+ descr: ["Run a function and rerun it whenever its dependencies change. Returns a handle that provides a `stop` method, which will prevent further reruns."],
+ args: [
+ {name: "func",
+ type: "Function",
+ descr: "The function to run. It receives one argument: the same handle that `Meteor.autorun` returns."}
+ ]
+};
+
Template.api.flush = {
id: "meteor_flush",
name: "Meteor.flush()",
diff --git a/docs/client/concepts.html b/docs/client/concepts.html
index ca1a4a4640..51c69c9d4d 100644
--- a/docs/client/concepts.html
+++ b/docs/client/concepts.html
@@ -199,9 +199,10 @@ application with error-prone logic.
These Meteor functions run your code in a reactive context:
+* [Templates](#templates)
* [`Meteor.render`](#meteor_render) and [`Meteor.renderList`](#meteor_renderlist)
* [`Meteor.autosubscribe`](#meteor_autosubscribe)
-* [Templates](#templates)
+* [`Meteor.autorun`](#meteor_autorun)
And the reactive data sources that can trigger changes are:
diff --git a/docs/client/docs.js b/docs/client/docs.js
index f7b096b7e4..68bf32d2ad 100644
--- a/docs/client/docs.js
+++ b/docs/client/docs.js
@@ -223,6 +223,7 @@ var toc = [
{instance: "context", name: "invalidate"}
],
{name: "Meteor.deps.Context.current", id: "current"},
+ "Meteor.autorun",
"Meteor.flush"
// ],
diff --git a/examples/other/template-demo/client/template-demo.js b/examples/other/template-demo/client/template-demo.js
index 92a490cafa..7959a9ca2c 100644
--- a/examples/other/template-demo/client/template-demo.js
+++ b/examples/other/template-demo/client/template-demo.js
@@ -207,7 +207,7 @@ Template.circles.rendered = function () {
if (! self.handle) {
d3.select(self.node).append("rect");
- self.handle = Meteor._autorun(function () {
+ self.handle = Meteor.autorun(function () {
var circle = d3.select(self.node).selectAll("circle")
.data(Circles.find({group: data.group}).fetch(),
function (d) { return d._id; });
diff --git a/packages/accounts-password/email_tests.js b/packages/accounts-password/email_tests.js
index fc7da6b80e..f3ff35701e 100644
--- a/packages/accounts-password/email_tests.js
+++ b/packages/accounts-password/email_tests.js
@@ -83,7 +83,7 @@
var waitUntilLoggedIn = function (test, expect) {
var unblockNextFunction = expect();
var quiesceCallback = function () {
- Meteor._autorun(function (handle) {
+ Meteor.autorun(function (handle) {
if (!Meteor.userLoaded()) return;
handle.stop();
unblockNextFunction();
diff --git a/packages/accounts-password/passwords_tests.js b/packages/accounts-password/passwords_tests.js
index 99177c2474..4f8e11e19c 100644
--- a/packages/accounts-password/passwords_tests.js
+++ b/packages/accounts-password/passwords_tests.js
@@ -17,7 +17,7 @@ if (Meteor.isClient) (function () {
test.equal(Meteor.user().username, someUsername);
});
return function () {
- Meteor._autorun(function(handle) {
+ Meteor.autorun(function(handle) {
if (!Meteor.userLoaded()) return;
handle.stop();
callWhenLoaded();
@@ -70,7 +70,7 @@ if (Meteor.isClient) (function () {
// Set up a reactive context that only refreshes when Meteor.user() is
// invalidated.
var user;
- var handle1 = Meteor._autorun(function () {
+ var handle1 = Meteor.autorun(function () {
user = Meteor.user();
});
// At the beginning, we're not logged in.
@@ -86,7 +86,7 @@ if (Meteor.isClient) (function () {
handle1.stop();
});
var waitForLoaded = expect(function () {
- Meteor._autorun(function(handle2) {
+ Meteor.autorun(function(handle2) {
if (!Meteor.userLoaded()) return;
handle2.stop();
callWhenLoaded();
diff --git a/packages/deps/deps-utils.js b/packages/deps/deps-utils.js
index 569d2deadd..cf39e39d6a 100644
--- a/packages/deps/deps-utils.js
+++ b/packages/deps/deps-utils.js
@@ -55,14 +55,14 @@
Meteor.deps._ContextSet = _ContextSet;
- ////////// Meteor._autorun
+ ////////// Meteor.autorun
// Run f(). Record its dependencies. Rerun it whenever the
// dependencies change.
//
// Returns an object with a stop() method. Call stop() to stop the
// rerunning. Also passes this object as an argument to f.
- Meteor._autorun = function (f) {
+ Meteor.autorun = function (f) {
var ctx;
var slain = false;
var handle = {
diff --git a/packages/deps/deps_tests.js b/packages/deps/deps_tests.js
new file mode 100644
index 0000000000..63e4e718ef
--- /dev/null
+++ b/packages/deps/deps_tests.js
@@ -0,0 +1,46 @@
+Tinytest.add('deps - autorun', function (test) {
+ var listeners = new Meteor.deps._ContextSet;
+ var x = 0;
+ var handle = Meteor.autorun(function (handle) {
+ listeners.addCurrentContext();
+ ++x;
+ });
+ test.equal(x, 1);
+ Meteor.flush();
+ test.equal(x, 1);
+ listeners.invalidateAll();
+ test.equal(x, 1);
+ Meteor.flush();
+ test.equal(x, 2);
+ listeners.invalidateAll();
+ test.equal(x, 2);
+ Meteor.flush();
+ test.equal(x, 3);
+ listeners.invalidateAll();
+ // Prevent the function from running further.
+ handle.stop();
+ Meteor.flush();
+ test.equal(x, 3);
+ listeners.invalidateAll();
+ Meteor.flush();
+ test.equal(x, 3);
+
+ Meteor.autorun(function (internalHandle) {
+ listeners.addCurrentContext();
+ ++x;
+ if (x == 6)
+ internalHandle.stop();
+ });
+ test.equal(x, 4);
+ listeners.invalidateAll();
+ Meteor.flush();
+ test.equal(x, 5);
+ listeners.invalidateAll();
+ // Increment to 6 and stop.
+ Meteor.flush();
+ test.equal(x, 6);
+ listeners.invalidateAll();
+ Meteor.flush();
+ // Still 6!
+ test.equal(x, 6);
+});
diff --git a/packages/deps/package.js b/packages/deps/package.js
index f21ff22073..a38a57f69e 100644
--- a/packages/deps/package.js
+++ b/packages/deps/package.js
@@ -11,3 +11,9 @@ Package.on_use(function (api, where) {
api.use('underscore', where);
api.add_files(['deps.js', 'deps-utils.js'], where);
});
+
+Package.on_test(function (api) {
+ api.use('tinytest');
+ api.use('deps');
+ api.add_files('deps_tests.js', 'client');
+});
diff --git a/packages/session/session_tests.js b/packages/session/session_tests.js
index 0a750b4086..d148914a1f 100644
--- a/packages/session/session_tests.js
+++ b/packages/session/session_tests.js
@@ -78,7 +78,7 @@
Tinytest.add('session - context invalidation for get', function (test) {
var xGetExecutions = 0;
- Meteor._autorun(function () {
+ Meteor.autorun(function () {
++xGetExecutions;
Session.get('x');
});
@@ -99,7 +99,7 @@
Tinytest.add('session - context invalidation for equals', function (test) {
var xEqualsExecutions = 0;
- Meteor._autorun(function () {
+ Meteor.autorun(function () {
++xEqualsExecutions;
Session.equals('x', 5);
});
@@ -132,7 +132,7 @@
function (test) {
// Make sure the special casing for equals undefined works.
var yEqualsExecutions = 0;
- Meteor._autorun(function () {
+ Meteor.autorun(function () {
++yEqualsExecutions;
Session.equals('y', undefined);
});
diff --git a/packages/spark/spark.js b/packages/spark/spark.js
index ee4d0b69be..861bf417c1 100644
--- a/packages/spark/spark.js
+++ b/packages/spark/spark.js
@@ -795,7 +795,7 @@ Spark.isolate = function (htmlFunc) {
var range;
var firstRun = true;
var retHtml;
- Meteor._autorun(function (handle) {
+ Meteor.autorun(function (handle) {
if (firstRun) {
retHtml = renderer.annotate(
htmlFunc(), Spark._ANNOTATION_ISOLATE,