When using RefluxJS stores with asynchronous actions, you can easily end up having race conditions between your actions.
Abstract description of the issue
For example, our store is in state X. An async action A is called from X, and before it finishes, another async action B is called, also from X. From here, no matter which action finishes first, it goes wrong.
- B finishes first with a state Y1, A finishes last and overwrites the state Y1 with Y2.
- A finishes first with a state Y2, B overwrites Y2 with Y1.
The desired behavior would be to have:
A B
X -> Y -> Z
Where B is not based on X, but on Y, and leads to a consistent Z state, instead of two actions based on the same state, leading to an inconsistent state:
A
X -> Y1 .--> Y2
\ /
'----'
B
Implemented example of the issue
I wrote a minimal working example, running with Node, of the problem I am talking about.
var Q = require('q');
var Reflux = require('reflux');
var RefluxPromise = require('reflux-promise');
Reflux.use(RefluxPromise(Q.Promise));
var AsyncActions = Reflux.createActions({
'add': { asyncResult: true }
});
var AsyncStore = Reflux.createStore({
init: function () {
// The state
this.counter = 0;
AsyncActions.add.listenAndPromise(this.onAdd, this);
},
// Increment counter after a delay
onAdd: function(n, delay) {
var that = this;
return apiAdd(this.counter, n, delay)
.then(function (newCounter) {
that.counter = newCounter;
that.trigger(that.counter);
});
}
});
// Simulate an API call, that makes the add computation. The delay
// parameter is used for testing.
// @return {Promise<Number>}
function apiAdd(counter, n, delay) {
var result = Q.defer();
setTimeout(function () {
result.resolve(counter + n);
}, delay);
return result.promise;
}
// Log the store triggers
AsyncStore.listen(console.log.bind(undefined, 'Triggered'));
// Add 3 after 1 seconds.
AsyncActions.add(3, 1000);
// Add 100 almost immediately
AsyncActions.add(100, 1);
// Console output:
// > Triggered 100
// > Triggered 3
// Desired output (queued actions):
// > Triggered 3
// > Triggered 103
With these dependencies in package.json
{
"dependencies": {
"q": "^1.3.0",
"reflux": "^0.3",
"reflux-promise": "^1"
}
}
Nature of the question
I expected RefluxJS to queue actions, but it doesn't. So I am looking for a way to order these actions correctly. But even if I managed to queue up these actions in some way (so B is issued after A) how can I be certain that, when A finishes, issuing B is still a valid action ? Maybe I am using RefluxJS the wrong way in the first place, and this scenario does not happen in a properly structured app.
Is queuing of the asynchronous actions (assuming this is possible within a Reflux app) the solution ? Or should we work on avoiding these scenarios in the first place, somehow ?