ember-concurrency icon indicating copy to clipboard operation
ember-concurrency copied to clipboard

Allow concurrency that depends on arguments passed to a task

Open orf opened this issue 8 years ago • 5 comments

Scenario:

You have an editor component that periodically auto-saves content using a concurrent task. If the task lives on the component then switching away from the page could loose saves.

So you move the task to a service. However this service is not very useful, as if you use something like keepLatest then only one save can run at a single time, and saves for other models can be lost. What you really want is a keepLatest that depends on an argument that is passed to the task.

I.E, if your first parameter to the task is model, you want the keepLatest functionality for each task launched with the same model instance. So you could have two concurrent tasks with different models running, but repeated calls to that task with the same model would trigger a queue or drop if one is already running.

I hope this makes sense, sorry if it doesn't! I was looking at the source code, couldn't it be possible to pass in the task instance or the arguments passed to the Scheduler.bufferPolicy.schedule function as a start, then maybe have a policy that accepts a function as it's first argument, returning a stable key from the given task arguments. Then the policy can apply the current logic to tasks that share that unique key?

In the case of the example above, you could perhaps do:

task(...).keepLatest(model=>`${model.get('constructor.modelName')}-${model.get('id')`)

orf avatar Jun 30 '17 13:06 orf

I've been experimenting with what I've been (badly) calling "contextual services" in ember-contextual-services to solve this in my app.

Basically it gives you services for your model instances.

Eg (from the readme):

//app/contextual-services/person.js
import ContextualService from 'ember-contextual-services';
import { task, timeout } from 'ember-concurrency';

export default ContextualService.extend({
  debouncedSave: task(function * () {
    yield timeout(1000)
    yield this.get('model').save();
  }).restartable();
});

rlivsey avatar Jun 30 '17 20:06 rlivsey

Perhaps this is a silly question, but if the task is something performed on the model, is there a reason the task shouldn't live on the model?

gabrielgrant avatar Apr 02 '18 05:04 gabrielgrant

@gabrielgrant That's actually not a silly question at all! In fact, that's a very good idea that solves the problem of different models for the same task. Thanks a lot for the tip.

c-emil avatar Oct 29 '18 20:10 c-emil

@c-emil I think one potential downside of this is that if the task is doing something fairly specific to a particular component (or other part of your app), it could create a coupling in the "wrong" direction -- my understanding is that generally Ember best practice is to keep the data layer fairly functionality-agnostic, and contain that functionality instead within controllers and/or components.

In the case the task is something more generally applicable, though (like a debounced save), yes, it does seem model-tasks could be useful

gabrielgrant avatar Oct 30 '18 11:10 gabrielgrant

@gabrielgrant I completely agree, having this functionality inside the model is not the first approach you'd take in many situations. But if it's, for example, just about debounced saving to backend, it's a very good solution. Let's say you wanna save every change that user makes on a model, but it's possible that user will change the model few times in few seconds (for example choosing a bunch of checkboxes as answers on a question - which is the model). Then this is great solution because you can debounce the saving.

c-emil avatar Oct 30 '18 12:10 c-emil