4

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.

  1. B finishes first with a state Y1, A finishes last and overwrites the state Y1 with Y2.
  2. 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 ?

Soreine
  • 121
  • 1
  • 5

1 Answers1

0

Your example seems like more of an issue with the concept of "source of truth" than anything else. You're storing the current state of the number ONLY client side, but ONLY updating it after receiving confirmation from the server side on an operation being done to it.

Of course that'll make issues. You're mixing the actions upon the number and the storage of the number in a weird way where there's no single source of truth for what the number is at any given moment. It's in limbo between the time when the action is called finished...and that's no good.

Either store the number client side, and every time you add to it, add to that number directly and then tell the server side what the new number is... (i.e. the client side is taking responsibility as the source of truth for the number while the client side runs)

OR store the number server side, and every time you up it with an action from the client side, the server returns the new updated number. (i.e. the source of truth for the number is completely server side).

Then, even if race issues occur, you still have a source of truth for what the number is, and that source can be checked and confirmed. For example, if the server side holds the source of truth for the number then the API can also return a timestamp for the status of that value every time it returns it, and you can check it against the last value you got from the API to make sure you're ACTUALLY using the newest value.

BryanGrezeszak
  • 577
  • 2
  • 8