Handle concurrent async updates to local state

sekiro999 :

I have a series of asynchronous calls that read from a local state S, perform some computation based on its current value, and return a new, update value of the local state S'

All this happens at runtime, so I have very little control over the order of these operations. This is a simplified version of what I have.

type State = {
  state: number
}

let localState: State = {
  state: 1000
}

const promiseTimeout = (time: number, value: number) => () => new Promise(
    (resolve: (n: number) => void) => setTimeout(resolve, time, value + time)
  );


const post: (n: number, currentState: State) => Promise<void> = (n, c) => promiseTimeout(n, c.state)()
  .then(res => {
    localState.state = res
    console.log(localState)
  })

post(1000, localState); // localState at call time is 1000
post(3000, localState); // localState at call time is still 1000
// when both promises resolve, the final value of localState will be 4000 instead of 5000

Playground link

This model is clearly broken, as both calls to post will read the same value of localState, while instead they should be performed sequentially.

If all calls were already determined at compile time, I could simply have something like

post(1000, localState)
  .then(() => post(3000, localState)) // localState at call time is now 2000

How would I go about solving this?

T.J. Crowder :

One approach is to have post hook into a promise rather than working directly on the state object. That promise could be stored in the state object itself. It starts out fulfilled with the state object. post updates it like this:

const post = (n, state) => {
    return state.promise = state.promise
        .then(state => {
            // ...do stuff here that updates (or replaces) `state`...
            return state;
        }));
};

Here's an example (in JavaScript, but you can add back the type annotations) using asyncAction (it's like your promiseTimeout, but without making it return a function we call immediately; not

"use strict";

let localState = {
    state: 1000
};
localState.promise = Promise.resolve(localState);

// I'm not sure why this *returns* a function that we
// have to call, but...
const promiseTimeout = (time, value) => () => new Promise((resolve) => setTimeout(resolve, time, value + time));
  
const post = (n, state) => {
    return state.promise = state.promise
        .then(state => promiseTimeout(n, state.state)().then(newValue => {
            state.state = newValue;
            console.log(state.state);
            return state;
        }));
};

console.log("Running...");
post(1000, localState); // localState at call time is 1000
post(3000, localState); // localState at call time is still 1000

Since each call to post synchronously replaces the promise with a new promise, the chain is built by the calls to post.

Here's that in TypeScript (with a bit of a hack in one place, you can probably improve that); link to the playground.

type State = {
  state: number,
  promise: Promise<State>
};

let localState: State = (() => {
    const s: Partial<State> = {
        state: 1000
    };
    // There's probably a better way to handle this than type assertions, but...
    s.promise = Promise.resolve(s as State);
    return s as State;
})();

// I'm not sure why this *returns* a function that we
// have to call, but...
const promiseTimeout = (time: number, value: number) => () => new Promise(
    (resolve: (n: number) => void) => setTimeout(resolve, time, value + time)
);

const post = (n: number, state: State): Promise<State> => {
    return state.promise = state.promise
        .then(state => promiseTimeout(n, state.state)().then(newValue => {
            state.state = newValue;
            console.log(state.state);
            return state;
        }));
};

console.log("Running...");
post(1000, localState); // localState at call time is 1000
post(3000, localState); // localState at call time is still 1000

It's worth noting that in situations like this where the state can be changed asynchronously like this, it's often worth producing a new state object when changing it rather than modifying the existing one — e.g., treat the state aspects as immutable.

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=7177&siteId=1