I’ve been working on a Meteor app with instant search functionality. When users to type data into an input box, the system updates a session value which kicks off a Meteor.subscribe.

Template.controls.events({
    'keyup #search': function(e) {
        Session.set('search', e.target.value);
    }
});

Meteor.autorun(function() {
    Meteor.subscribe('my-collection', Session.get('search'));
});

While this worked, triggering a new subscribe for every keypress put too much of an unneeded strain on the system. Typing the word “test” triggered 4 different subscriptions, and the first 3 sets of subscription results were thrown out in a fraction of a second. I needed to limit the rate at which I was triggering my new subscriptions and subsequent database queries. A great way to do that is with Lo-Dash’s debounce method.

Debounce Meteor.subscribe

My initial idea was to debounce the Meteor.subscribe function used within the Meteor.autorun callback. Since the session variables being tracked by the Tracker computation could be updated in other places in the app as well, I figured this would be a clean way to limit excessive subscriptions being made to the server.

I changed my code to look like this:

var debouncedSubscribe = _.debounce(Meteor.subscribe, 300);
Meteor.autorun(function() {
    debouncedSubscribe('my-collection', Session.get('search'));
});

This had a very interesting affect on my application. While changing the session variable did trigger a new subscription, and the call was being debounced as expected, I noticed that old subscription results were being kept around on the client. The collection was starting to balloon in size.

Down to Debugging

I fired up my trusty Chrome Dev Tools and started debugging within the subscribe method itself in livedata_connection.js. After comparing behavior with a normal subscription invocation and a debounced invocation, the problem made itself known on line 571.

When it was executed as a debounce callback, the Meteor.subscribe call was no longer part of a computation because it is executed asynchronously. Tracker.active was returning false within the context of the debounce callback. This means that the Tracker.onInvalidate and Tracker.afterFlush callbacks were never initiated within the subscribe call as they would have been if subscribe were called from directly within a computation. That caused the subscription to never “stop” and its subscription data stayed around forever. Effectively, I was piling up new subscriptions every time the search string changed.

Meteor.subscribe('my-collection', 't');
Meteor.subscribe('my-collection', 'te');
Meteor.subscribe('my-collection', 'tes');
Meteor.subscribe('my-collection', 'test');
...

The Solution

I spent some time trying to find a way to run an asynchronous callback under an existing computation, but I wasn’t able to find a good way to do this. Ultimately, my solution was to not debounce the Meteor.subscribe call, but to debounce the keyup event handler:

Template.controls.events({
    'keyup #search': _.debounce(function(e) {
        Session.set('search', e.target.value);
    }, 300)
});

Meteor.autorun(function() {
    Meteor.subscribe('my-collection', Session.get('search'));
});