RFC Proposal: Expose low-level coroutine functions
See original GitHub issueExpose low-level coroutine functions
In the same vein of tj/co it would be helpful to use coroutine patterns in places outside of tasks.
Much of ember uses promises. Knowing that promises improve the state of asynchronous code, they also introduce a level of cognitive complexity that is difficult to maintain in many situations. e-c tasks and coroutines are interchangeable with promises.
This is a proposal to offer the same interchange functionality that e-c uses under the hood to manage tasks to the user so they can use them in places where a task would be inappropriate.
Possible use cases (non-exhaustive list)
- Asynchronous test cases
beforeModel
,model
, andafterModel
hooks- Custom networking services
- Asynchronous procedures in build scripts
ember generate server
code- Addon code (scripts used as part of blueprints and/or build hooks)
At the moment if you want to use coroutines in these cases you have to npm install co --save-dev
which I think is node specific and not included in the final ember output.
Using other coroutine libraries is duplicating efforts since e-c already implements these under the hood.
Proposed API (A)
import { spawn } from 'ember-concurrency';
let promise1 = spawn(function * () {}); // => Promise
let wrapped = spawn.wrap(function * () {}); // => Function
let promise2 = wrapped(); // => Promise
Proposed API (B)
import { coroutine } from 'ember-concurrency';
let promise1 = coroutine(function * () {}); // => Promise
let wrapped = coroutine.wrap(function * () {}); // => Function
let promise2 = wrapped(); // => Promise
Examples
test('…', spawn.wrap(function * (assert) {
yield visit('…');
yield clickOn('button');
assert.ok($('button').is(':disabled'));
});
model: spawn.wrap(function * () {
let data = yield $.ajax({url: '…'});
let data2 = yield $.ajax({url: '…', data});
return data2.map(itemData => MyItem.create(itemData));
})
model() {
let users = spawn(function * () {
let config = yield $.ajax({url: '/config'});
return Ember.get(config, 'memberships.users');
});
let posts = $.ajax({url: '/posts'});
return RSVP.hash({users, posts});
}
var spawn = require('ember-concurrency/spawn');
function wrap(gen) {
let fn = spawn.wrap(gen);
return function (req, res, next) {
return fn(req, res, next).catch(next);
};
}
module.exports = function (app) {
app.get('/api/post', wrap(function * (req, res) {
let data = {
meta: { foo: 'bar' },
data: yield getDataAsync()
};
res.setHeader('Content-Type', 'application.json');
res.send(JSON.stringify(data));
}));
};
Issue Analytics
- State:
- Created 7 years ago
- Comments:7 (4 by maintainers)
Top GitHub Comments
Anyone interested in implementation progress on this RFC can follow along w the
to-function
branch: https://github.com/machty/ember-concurrency/tree/to-functionWe’ve been discussing this RFC in the e-concurrency slack channel, and it seems like consensus is building for the following:
.toFunction()
Instead of having a separate
.wrap
function / chained method, we could just supply a.toFunction()
task modifier. Unlike the other task modifiers (e.g..drop()
/.restartable()
),.toFunction()
wouldn’t just modify the TaskProperty descriptor but would actually produce an immediately-usable Function. This function could be callable on its own, or as a method of an object, e.g.The functions produced by
.toFunction()
share the same semantics as normal tasks, with the exception that there’s no concept of “top-level” Task state, like.isRunning
or.isIdle
. The reason for this is thatSupporting this will involve some internal reorganization; right now, task state lives on the Task object the each instance of a class that declares task, e.g. if
FooComponent
declaresmyTask: task(...)
, the state that tracks whethermyTask
is running lives inside an instance of Task on an instance of FooComponent. In order to support task functions preserving the same concurrency semantics as plain oltask()
s, we’ll move this state to some shared, global cache keyed on(taskFunction, hostObject)
. I imagine there is a clever way to build this cache that leverages WeakMap if it exists, otherwise the cache can garbage collecthostObject
s for whichisDestroyed
is true.Concurrency semantics for task functions
The behavior of the above example is the same whether we’re using POJOs or Ember.Objects. The interesting bit here is that we’re using
.restartable()
here, which conceptually only makes sense when a task has an “owner”. With task functions, the “owner” is whatever thethis
context is when you invoke the task function. This why in the above example, callingobject1.debouncedFn()
doesn’t cancel the task instance returned fromobject0.debouncedFn()
, but multiple calls toobject0.debouncedFn()
will cancel previous iterations. Functions called with a falsy context will share the same global context (chances are, in most cases, you’ll apply task modifiers to task functions that live on objects as methods).