I need some help in figuring out where would be the best place to poll for my model and what is the best way of going about doing this(I’ve been at this for about a week now).
Here is my situation. There are two routes of interest: /queries/:query_id and /queries/:query_id/results/:result_id The first route show you information about a query, its parameters and allows you to execute a query. Each query can take a certain amount of time to finish. How the frontend determines if a query is finished is by reading the response that we get from polling the backend and checking if status === "COMPLETED"
When a query is executed, a bootstrap tab appears and takes you to /queries/:query_id/results/:result_id and each time that query is executed, a new tab appears and takes you a new result route. Each tab will have to deal with polling the backend for the status of that query execution( result_id ). When I switch between tabs, I want to abort polling for the previous result and start polling for the result that we are currently on. I also want the tab to be continuously updated while polling and not wait until the query is finished.
Initially I’ve tried to poll in my router.
import Router from '@ember/router';
import { task } from "ember-concurrency"
export default Router.extend({
fetchResultTask: task(function*(resultId){
while(true) {
yield timeout(1000);
const result = this.get("store").findRecord("result", resultId)
if(result.status === "COMPLETED") {
break;
}
else {
return result // Send this result to result component so it can update whatever part of the ui it needs to update.
}
}
}).cancelOn("willTransition")
model(params) {
return {
result: this.get("fetchResultTask").perform(params.result_id)
}
}
});
{{result-component result=model.result}} <!-- I want result to continously be updated until status is completed--->
The problem with this is that this will poll only once and wont continuously update my result model until it is finished. So maybe I should update in either in the result controller or result component?
You are so close and after much debate with myself I think you are on the right track. I wrote a long post on how this can work: Two-Tasks Routes in Ember.
In your case the previousTask is not something you care about but the idea remains the same. The key is that you do not return a promise in the model hook as that will pause the route transition till the task is complete. Instead return a scalar value so Ember sees that the model has resolved (even thought it hasn’t) Than use ember-concurrency’s derived state to manage the UI.
app/routes/my-route.js
import Router from '@ember/router';
import { task } from 'ember-concurrency';
export default Router.extend({
model(params) {
let resultTask = this.get('fetchResultTask').perform(params.result_id);
// Tell Ember's uncaught promise handler that we got this and to not send
// the error to the console
resultTask.catch(() => {});
// Return the original task to get the derived state not the result of the
// above catch as the return of a catch is a Promise not a TaskInstance.
return { resultTask };
},
findResult(resultId) {
return this.get('store').findRecord('result', resultId);
},
fetchResultTask: task(function * (resultId) {
// You need to yield in order to get the result otherwise result is a
// Promise<Object> and not an Object
let result = yield this.findRecord(resultId);
while (result.status !== 'COMPLETED') {
yield timeout(1000);
result = yield this.findRecord(resultId);
}
return result;
})
// The correct event to cancel in a route is 'deactivate'
.cancelOn('deactivate')
// Cancel and start again each time the model() hook is called which happens when the
// result_id changes
.restartable()
});
app/controllers/my-route.js
import Controller from '@ember/controller';
import { reads } from '@ember/object/computed';
// I like to alias my model in the controller. Mainly `model` is too
// nondescript for me. In this case the use of an Object adds another level
// which would be a pain to use throughout the template code. An alias is best:
export default Controller.extend({
isPolling: reads('model.resultTask.isRunning'),
result: reads('model.resultTask.value'),
error: reads('model.resultTask.error')
});
app/templates/my-route.hbs
{{#if this.isPolling}}
Loading&helip;
{{else if this.error}}
Oh Nos!!!! {{this.error}}
{{else}}
The results: {{this.result}}
{{/if}}
Thanks for the reply. Will try it out and see what happens. In the fetchResultTask, I think you meant yield this.findResult instead of findRecord correct? Because I dont see it being used anywhere. Also In the documentation for routing, It says that deactivate is called when I completely leave the route but I want to abort when I go to a new tab. Remember that each tab will go to /queries/:query_id/results/:result_id and so deactivate will not get hit when switching. So that is why I think is the correct one here no? Also I need to emphasize that I’m not talking about browser tabs too.
I should be said that this could also be accomplished in a Component. However, I answered above using a route task because based on the description it felt the route was the appropriate place to ask for data. It also means that the lifecycle matches the expectations for caching and optimization. If this data was not part of the URL (params.result_id) Then I might argue the a component would be better suited. Basically when the URL is the source of truth the data should be fetched/constructed in the route because Ember has done a lot of work to handle most of the edge cases for you. But when the URL is not part of the truth (for example cases where you want to grab data that is not directly bookmark-able like say summary data vs detail views) then the component lifecycles do wonderful things and can be easier to reason about.
One more concern. Wouldn’t fetchResultTask only return the last value from the task and not each one that it receives from calling this.store.findRecord? Like instead of showing a loading indicator, just return result and display any new data that it might have received from the backend. That is what I was trying to achieve in my initial post.
Also You are correct about deactivate. However, partly so. You want to cancelOn('deactivate') to stop the polling when you leave the route. To handle when you transition from one result to another (changing the result_id) simply place a .restartable() on the task. This will cancel and restart the task when the model() is called and that happens when the params change.
Yes your right. Using restartable with cancelOn(‘deactivate’) should do the trick. My previous comment was talking about modifying the code that I showed to implement a UI technique called progressive disclosure(I want to show data as it pulled from the backend). Going back to initial code for fetchResultTask(With some modification based on your suggestions).
fetchResultTask: task(function*(resultId){
while(true) {
yield timeout(1000);
const result = this.get("store").findRecord("result", resultId)
if(result.status === "COMPLETED") {
break;
}
else {
return result // Send this result to result component so it can update whatever part of the ui it needs to update.
}
}
}).restartable().cancelOn("deactivate")
In might be the case that some attributes from the result model are ready to be displayed to the user and other still need to be polled. So instead of showing a load indicator, I want to show those attributes that can be displayed. This is why in my code above I was returning result before it completed. But in your example, we poll until we are finished rather than polling until we are finished while simultaneously displaying any new data that may have arrived.
This was not clear in your original post nor the example code provided. An option could be this idea where the route starts the task as before but exposes the result using an encapsulated task:
route task:
fetchResultTask: task({
store: service(),
updateResult(resultId) {
let result = this.get('store').findRecord('result', resultId);
this.set('result', result);
return result;
},
*perform(resultId) {
let result = yield this.updateResult(resultId);
while (result.status !== 'COMPLETED') {
yield timeout(1000);
result = yield this.updateResult(resultId);
}
}
})
.cancelOn('deactivate')
.restartable(),
Then the current result would be in model.resultTask.result which will update each time.
The more I think about your situation I think the above here is the best solution for you. There is the alternative of moving the polling into a component here:
Component based code
app/pods/components/result-poller/component.js
import Component from '@ember/component';
import { task, timeout } from 'ember-concurrency';
export default Component.extend({
tagName: '',
updateResult() {
let result = this.get('store').findRecord('result', this.get('resultId'));
this.set('result', result);
return result;
},
fetchResultTask: task(function * () {
// You need to yield in order to get the result otherwise result is a
// Promise<Object> and not an Object
let result = yield this.updateResult();
while (result.status !== 'COMPLETED') {
yield timeout(1000);
result = yield this.updateResult();
}
}).drop(),
didInsertElement() {
this._super(...arguments);
this.get('fetchResultTask').perform();
}
});
Alright thanks. Sorry If I wasn’t clear. I will first try it out in the router because I do agree with you that the fetching of the data should be in router because of all the edge cases that it handles for you. Will get back to you on my progress.