1

I need help on developing a good strategy for polling in the router. I have a route queries/:query_id/results/:result_id that I transition to whenever the user executes a query. In this I route I need to load two things: the result model that is associated with this route and a table object using the url inside the result model. The problem is, if a query is long running I need to poll and ask the server if the query is finished. Only then can I download the table. I'm using ember concurrency to do all my polling and it works great except for a small edge case. This edge case has to do with the fact that If my polling function gets canceled after it is finished and while it downloads the table, then it will get stuck saying "loading the table" because it only triggers the polling when the status of the query is not completed. The download of the table happens inside the polling function but only when the query is finished. I'm doing all my data loading in the result route so maybe someone can offer some alternatives to do this. I also need to mention that each table will be displayed in a separate tab(bootstrap tabs). Because of this I want to minimize the amount of times I fetch the table(reason why I'm pushing it to the store) when I switch between tabs as each tab is a link to a new result route.

Relevant code in the result route


import Route from "@ember/routing/route";
import { inject as service } from "@ember/service";
import { reject } from "rsvp";
import { task } from "ember-concurrency";
import { encodeGetParams } from "gtweb-webapp-v2/utils";

export default Route.extend({
  poller: service("poller"),

  fetchResults: task(function*(result_id) {
    try {
      const result = yield this.store.findRecord("result", result_id);

      if (result.status === "COMPLETED") {
        const adapter = this.store.adapterFor("application");
        const queryString = encodeGetParams({ parseValues: true, max: 25 });

        const table = {
          table: yield adapter.fetch(
            `${result._links.table.href}?` + queryString
          )
        };

        // Cache the table that we fetched so that we dont have to fetch again if we come back to this route.
        this.store.push({
          data: [
            {
              id: result_id,
              type: "result",
              attributes: table,
              relationships: {}
            }
          ]
        });

        return true;
      }
    } catch (err) {
      return reject(err);
    }
  }),

  model(params) {
    const result = this.store.peekRecord("result", params.result_id);

    if (!result || result.status !== "COMPLETED") {
      const poller = this.get("poller");
      poller.startTask(this.get("fetchResults"), {
        pollingArgs: [params.result_id],
        onComplete: () => {
          this.refresh(); // If we finish polling or have timeout, refresh the route.
        }
      });
    }

    return result;
  },

  setupController(controller, model) {
    const query = { title: this.modelFor("queries.query").get("title") };
    controller.set("query", query);
    this._super(controller, model);
  },

  actions: {
    willTransition() {
      const poller = this.get("poller");
      poller.abort(); // Task was canceled because we are moving to a new result route.
    }
  }
});

Idea

One idea is probably creating a separate route for loading the table i.e queries/:query_id/results/:result_id/:table_id and only transition to this once the query is completed. From there I can safely load the table. Only problem I have with this is that the result route will simply be involved with loading the result. There will be no components that will be render in the result route; only in the table route.

Luis Averhoff
  • 887
  • 2
  • 11
  • 22
  • It would help if you produced a reproduction in ember-twiddle or codesandbox.io – Gaurav Jul 16 '19 at 15:14
  • Maybe I missed something but it seems like you don't wait in model hook until polling is completed. Is that intended? What is the reason for doing a refresh if it's completed and not wait until it's completed and resolving with loaded model afterwards? – jelhan Jul 17 '19 at 07:37
  • @jelhan There is no waiting in the model hook. I only use it to start the task nothing more. The polling stops once I hit a limit and I refresh to check to see if the query is completed or If I need to start another task. I peek at the store so I dont make a network request. Hope that helps. – Luis Averhoff Jul 17 '19 at 20:10
  • It would be helpful if you either include an Ember Twiddle / CodeSandbox and/or provide more information why the route should resolve before the data is loaded. It's difficult for me to understand your design decisions. I would have expected a Promise being returned by polling service that resolves with the data as soon as it's available. – jelhan Jul 18 '19 at 07:27
  • @jelhan My goal as of now is to load the data from my backend into a table and display the data as it is coming in without waiting for the entire query to finish. I also dont want to display all the data at once so I'm paginating the table as well. So when the table first loads up, there is spinner that is constantly shown until the query is completed and is constantly appending data to the table. Once it is done, just show all the data for that page if applicable. This is my end goal. Right now I wait until the query is finished, update the table with the first page and refresh the route. – Luis Averhoff Jul 18 '19 at 11:27
  • @jelhan In regards to the poller, all it does it executes the function continously until the function returns something that evaluates it to true. It doesn't store any data because I believe that is what the ember store is for no? – Luis Averhoff Jul 18 '19 at 11:29
  • @jelhan I'll try to provide a fiddle showcasing with I'm trying to do this weekend. – Luis Averhoff Jul 18 '19 at 11:31
  • 1
    I would move the entire polling logic to the controller. then use the state booleans on the task in the template – Lux Jul 18 '19 at 18:45
  • @Lux I did the polling in the router because many people in the ember community say that you should do your data fetching in your routes(if not possible, do it in your data loading component). Why the controller though? – Luis Averhoff Jul 18 '19 at 19:11
  • 1
    Using the controller or a container component doesn't make a big difference. They are very similar concepts in my opinion. Controller gives you the benefit of being a singleton which is useful for some scenarios. Container components could be easier to reason about. But using the model hook if you don't want to block the rendering (and use a loading substate) while the data is fetched, feels wrong to me. – jelhan Jul 18 '19 at 21:01
  • @jelhan If I dont use the model hook, then how I can tell the controller or component, "Hey the query is done and the model has been updated"? With the model hook, all I have to do is refresh once the task is done in the router and the router will fetch from the store via peekRecord. This is one of the main gripes I've been wrestling with this problem and what I need suggestions on. – Luis Averhoff Jul 18 '19 at 21:40
  • You could set a property as soon as your polling service received new data. As always you need to use `this.set('prop', value)` to inform Ember's State Management about a change. If already using Octane you need to decorate the property instead with `@tracked`. Your question seems to be a little bit to vague to be addressed in one answer. You may want to open more focused question on separate topics. How to use async data loading in components would be one of them. The pros/cons of using controller or container component to load data would be another one. – jelhan Jul 19 '19 at 08:01
  • @jelhan Ok cool thanks for the tip. Another question in regards to fetching data in the router versus with a controller or component is this. Say I have a really long query and my polling task timeout and so I need to start the task all over again. How can effectively restart my polling task if I do my fetching in a component or controller? In the router, I can simply pass it a callback when it timeout to refresh the route like this `onTimeout: () => {this.refresh()}`. One idea would to be use an observer on the data received but I dont know if this would be a good use case for it. – Luis Averhoff Jul 19 '19 at 11:14
  • My bad, I dont know what I was thinking with the observer. I should have said that I can have a timeout property in my poller and use that to detect when the poller timeout. Then I can probably check this property in the didReceiveAttrs(which acts like an observer really) life cycle method to see if it is true and poll again. – Luis Averhoff Jul 19 '19 at 11:28

1 Answers1

0

Ola @Luis thanks for your question

I don't know exactly what you're trying to do with your code but it seems a tiny bit complicated for what you're trying to achieve because you're using ember-data we can simplify this problem significantly.

The first thing that we can rely on is the fact that when you call peekRecord() or findRecord() you are actually returning a Record that will be kept up to date when any query with the same ID updates the store.

Using this one piece of knowledge we can simplify the polling structure quite significantly. Here is an example Route that I have created:

import Route from '@ember/routing/route';

export default Route.extend({
  async model(params) {
    // this is just a hack for the example and should really be something that
    // is purely determined by the backend
    this.stopTime = (Date.now() / 1000) + 20;

    // get the result but don't return it yet
    let result = await this.store.findRecord('time', params.city);

    // setup my poling interval
    let interval = setInterval(async () => {
      let newResult = await this.store.findRecord('time', params.city);

      // this is where you check the status of your results
      if(newResult.unixtime > this.stopTime) {
        clearInterval(interval);
      }
    }, 10000);

    // save it for later so we can cencel it in the willTransition()
    this.set('interval', interval);

    return result;
  },

  actions: {
    willTransition() {
      let interval = this.get('interval');

      if(interval) {
        clearInterval(interval);
      }
    }
  }
});

I have created a time model, adapter, and serializer that queries a public time API which will return the current time for a particular timezone. As you can see, I am storing the initial result and before I return it, then I have set up my standard JavaScript interval with setInterval() that will poll every 10 seconds to update the time. I then set the interval on the route so I can cancel it when we leave.

(Note: I have only set the interval to 10 seconds because I got rate limited by the service for polling every 1 second you can set this to however often you want to check for data)

I have edited my example to also include another way to stop polling based on the result of the query itself in the interval function. This is a little bit of a toy example but you can edit this to fit your needs.

As I said in the beginning, we are relying on the way that ember-data works for this to be seamless. Because findRecord() and peekRecord() always point to the same record you don't need to do anything special if a subsequent request to your data includes the results that you need.

I hope this helps!

real_ate
  • 10,861
  • 3
  • 27
  • 48
  • Hey @real_ate, I know it seems kinda complicated so let me try to bring this to light. I have a model called result and inside of it, I have a nested a object called table. When I call on findrecord, it may be the case that the table is ready to be downloaded. If it is, great we can download the table but I only want to do it once. If not we continue to poll for the table. The reason for this is because I'm using bootstrap tabs and each tab represents a table to show. When I switch tabs I dont want to have download the table if I've already done it. Hope that helps. – Luis Averhoff Jul 24 '19 at 16:29
  • Also your solutions polls indefinitely. I have to stop when the query is finished. What I'm trying to do is download the table when new rows become available. I dont want to wait until the query finished to download the table. I think this technique is called progressive disclosure where you show data bit by bit as it comes in. – Luis Averhoff Jul 24 '19 at 16:32
  • Hey @LuisAverhoff really there should be enough in this answer to be able to extract what you need for your specific solution I will edit it a little bit to make it more specific to your use case but without your full frontend and backend I'm not going to be able to give you the exact answer you will need – real_ate Jul 25 '19 at 07:27