Processing Multiple Collection Updates in Autorun Before Updating Template UI

#1

I’ve been using Meteor for years and I finally found a Blaze autorun issue that has stumped me. The problem is waiting on multiple Collection updates in a single template before updating the UI.

Here’s an example illustrating the problem:

I’m working on a collaborative document editor. Think Google Docs. I have a Papers collection with a page field that holds the current page the group is on. At any time, any members of the group can update the paper to a the next/previous page.

When collaboratively working on a paper, any user can enable several different “info views” on the paper. One view, for example, is a “Author Information” view that kicks out a side-column to the right of the paper that has a bunch of meta-data about the authors of the paper. This data is in a Authors Collection and contains a showAuthors field in the Authors collection along with the data. There’s another view that can be enabled that shows the formulas that make up any embedded charts. That’s a collection called Formulas and has a showFormulas field along with the data.

Any time someone changes to a new page, the “Authors” and “Formulas” side-columns should automatically close and have to be reopened by someone on the new page of the paper.

So in the page viewing template there is an autorun that looks like this:

instance.autorun(function() {
   let currentData = Template.currentData();

   //  Listen for page to update
   instance.currentPage.set(currentData.page);   

   //  Listen for views to update
   instance.showAuthors.set(currentData.authors.showAuthors);
   instance.showFormulas.set(currentData.formulas.showFormulas);
});

So the idea is that we’re reactively listening for anyone to enable any of the side-column views and also update the page. The documents Authors and Formulas object are loaded onto the main Papers document in the publication. SIDE NOTE: I’m using ReactiveVars to control reactivity because the user can put the paper into a “local editing mode” and control the page and views without updating everyone else or receiving the server state, so I don’t connect directly to the actual data fields in helpers. Instead I use currentData() in an autorun. I use intermediary ReactiveVars to control the UI. This doesn’t/shouldn’t affect this issue.

Whenever a page is changed by any user from the server, the two views need to disable. So on the server for the Papers collection I’m using matb33:meteor-collection-hooks and have a hook to hide the Authors and Formulas anytime a new page is updated:

Papers.before.update(function (userId, doc, fieldNames, modifier, options) {
   if(modifier.$set && modifier.$set.page && modifier.$set.page != doc.page) {
      Authors.update({paperId: doc.paperId}, {$set: {showAuthors: false}});
      Formulas.update({paperId: doc.paperId}, {$set: {showFormulas: false}});
   }
});

So what ends up happening is the above autorun is ran three times. Once for the Authors update, once for the Formulas update, and once for the Papers update.

The problem is in the UI this all happens in a glitchy linear order. First the “Authors” side-column hides, then the “Formulas” side-column hides, then the “Page” updates to the next page (I know that might sound cool - but in this case it looks really bad). What I’m trying to achieve is to somehow wait for all these updates to happen on the client then to a single update pass to the template. The above example is simplified and is actually more complex in my actual app. But this illustrates the issue.

I don’t need to use a loader because I actually load ten pages of the paper, the authors, and the formulas at a time. So there is no loading delay. All the data is already loaded. What I’m looking for is some way to not update the template until all three autoruns have completed. Even if I separated the above autorun into three separate autorun blocks, you’d still have the same problem. Because the template is sourced from three different Collections, each time you update each of those Collections, the template updates one by one. Is there any way to handle this in Meteor? I tried playing with Tracker.afterFlush but it always fires after each autorun fires. There doesn’t seem to be a way to wait for multiple autoruns to finish.

Trying to use some kind loader ReactiveVar hack doesn’t work either. As the above autorun has to also allow any users to show or hide any of the side-columns at anytime. So there’s no way to know if hiding a side-column is because a user directly wants to hide it or if it’s being hidden by the page hook because a new page is being updated. I even thought about using a setTimeout to wait on multiple autorun updates but again… too hacky.

I guess in this use-case need to know why data is being updated - not just the the update of the data. Then I could shut off all the views in one pass of the autorun but there’s no way of knowing why a view is being shut off.

I know one solution is to move the two fields over to the Papers collection, then the update could come in one fell swoop to a single collection. But I’m curious if there’s a solution for this type of data design. As this could happen a lot in my app.

2 Likes
#2

One possible solution may be to add another “dummy” collection and update that on a Papers.after.update as well:

Papers.before.update(function (userId, doc, fieldNames, modifier, options) {
   if(modifier.$set && modifier.$set.page && modifier.$set.page != doc.page) {
      Authors.update({paperId: doc.paperId}, {$set: {showAuthors: false}});
      Formulas.update({paperId: doc.paperId}, {$set: {showFormulas: false}});
   }
});

Papers.after.update(function (userId, doc, fieldNames, modifier, options) {
  Dummy.update(...);
});

and then use the dummy collection as the reactive control in the client.

The only other alternative that springs to mind is to wire up collection observers to a reactive control “switch” to achieve the same result (a single, trackable thing).

1 Like
#3

Thanks Rob. I was hoping there was some Tracker magic designed to solve this issue. Guess not.

One interesting note is whichever user actually triggers the page change, their client works correctly. As on the client where the page change is triggered, you can call the updates you want as a client update with:

Authors.update({paperId: doc.paperId}, {$set: {showAuthors: false}});
Formulas.update({paperId: doc.paperId}, {$set: {showFormulas: false}});

And optimistic UI (I assume) takes care of making currentData() match what was set in the client. So the template updates in one pass exactly how you want. Then when the Autoruns fire from the server updates, they simply reinforce what was already set in the client.

But… if the update comes from another user, you get the staccato update situation I described above.

1 Like
#4

Hi, I’m sitting on the couch right now and maybe didn’t catch every detail of this issue exactly…

But what I did when I experienced a similar situation where the view depended on a bunch of different parameters and I did want to control the rendering precisely:

I stored/cached the results to render in arrays / objects in reactive vars and passed those to the template eg. using helpers or something similar (I like TemplateController and its state helper but that’s just me).

This way you can decide when to exactly update the information. It can be tricky but I think that’s Probably a possible solution for most cases…

This way it’s eg. Possible to unsubscribe/resubscribe to Publications without the results disappearing for a moment, and I guess with a bit of imagination you can control the view all the way round.

I hope this helps… :slight_smile:

#5

Is the concern that page, authors and formulas are updated once, but “flash” three times? If they are used in separate areas, I’d split it into three autorun blocks, and use Tracker.guard to ensure they are different, something like this:

instance.autorun(() => {
   instance.currentPage.set(Tracker.guard(() => Template.currentData().page));
});

However, if you refer to all three in the same blocks, this won’t help

#6

@DanielDornhardt @znewsham Thanks for the responses but I don’t think either of those solves the issue.

The issue is that I don’t want the template’s UI to do the following:

  • Refresh Step 1. Hide the “Authors” side-column block (because of an update)
  • Refresh Step 2. Hide the “Formulas” side-column block (because of an update)
  • Refresh Step 3. Show the next page of the document (because of an update)

I want to somehow make the template’s UI just do:

  • Refresh Step 1: Hide the “Authors” and “Formulas” side-column blocks and show the next page of the document (because it knows to wait on all these updates that are coming in)

No poppiness. Total UI control.

This issue @DanielDornhardt is even if I use some kind of ReactiveVar faux-loader-controller, the above Autorun still has to run three times using the above pattern. There’s no way to know if hiding one of the side-columns is because some user is hiding it (Use Case #1) or if some user is updating the question in which is gets hidden before the Papers.before.update hook completes (Use Case #2). If it’s Use Case #1, then the template UI should update right after the Autorun because there’s no other updates coming. If it’s Use Case #2, it should wait until Step 2 and Step 3.

The second use-case is the issue. As that first update comes in to hide a side-column, the Autorun completes, and any faux-loader-controller would then update the UI. How would it know that there’s two more updates coming from the server and to wait to update the template’s UI? This issue still persists @znewsham even if you break up the above up until three Autorun blocks. The only hacky way seems to do some kind of setTimeout to wait a half-second on future Autorun calls to complete.

I figured there was some Tracker function that could detect if future updates are coming right now in the queue and somehow wait to update the template UI.

#7

Can you show us how you build the data to the current component? If those updates are happening client side, I would expect autorun to batch them. How quickly do these updates come in? A fairly common pattern is to throttle frequently occurring updates to stop this kind of UI flicker.

Another alternative would be to change you components that accept this data, so they don’t flicker when the data doesn’t change. I’ve only found this type of reactivity to be a problem when you call heavy server side methods on data changes, in these cases Tracker.guard has served me well.

#8

It’s very basic and simple. When the user updates the page from their client there is no problem. It’s when your receive someone else’s page update and the two updates occur on the collection hook.

After much thought, the essential problem is how do you combine “Hide Window 1 Autorun” and “Hide Window 2 Autorun” into a single UI-friendly “Hide Windows Autorun” when each is driven by a different flag in separate Collections that have to be updated in two different Mongo update commands that are not sourced from that client but from the server by another collaborative user (while still preserving their ability to be individually hidden one at a time).

I just realized one solution. You could wire up one of the DDP stream packages (or redis-oplog’s Vent tool) and just send a custom message down to the client before the two updates in the collection hook are sent. Then when that message is received, the client would know to update everything in one fell swoop by setting some kind of flag, etc. Basically send a warning of what’s coming.

Without a custom message and purely relying on data there’s no way to know if the data update means to just hide this one side-column and update the UI or hide this side-column BUT there’s many more UI updates coming based on database flags so wait until they all get updated, then update the UI because the page was updated. The template can’t know the difference.

1 Like