What is an effective strategy for polling in the router in Emberjs?

585 Views Asked by At

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.

1

There are 1 best solutions below

3
real_ate On

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!