Managing simultaneous asynchronous updates to the local state

There is a scenario where a series of asynchronous calls are made that read from a local state S, perform certain computations based on its current value, and return an updated value of the local state S'.

All these operations occur at runtime, with limited control over their order. Here is a simplified representation:

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); // initial value of localState is 1000
post(3000, localState); // initial value of localState is still 1000
// when both promises resolve, the final value of localState will be 4000 instead of 5000

Playground link

This model clearly has an issue, as both calls to post are reading the same value of localState when they should be executed sequentially.

If all calls were determined at compile time, the solution would involve chaining promises like this:

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

What would be the approach to fix this situation?

Answer №1

To create a more efficient solution, consider having the post function interact with a promise instead of directly manipulating the state object. This promise can be stored within the state object itself and initialized as fulfilled with the current state. The post function can then update it like so:

const post = (n, state) => {
    return state.promise = state.promise
        .then(state => {
            // ...perform necessary operations to modify the `state`...
            return state;
        }));
};

Below is an example written in JavaScript using asyncAction, which functions similarly to your promiseTimeout but without returning an immediately invoked function:

"use strict";

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

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); 
post(3000, localState); 

By synchronously replacing the promise with each call to post, a chain is established through successive calls.

The TypeScript version of this code is available here.

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

let localState: State = (() => {
    const s: Partial<State> = {
        state: 1000
    };
    
    s.promise = Promise.resolve(s as State);
    return s as State;
})();

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); 
post(3000, localState); 

It's important to note that when dealing with states that may change asynchronously, it's beneficial to create a fresh state object rather than modifying the existing one - treating the state as immutable can prevent unintended side effects.

Answer №2

Encountering a recurring issue, I devised a solution by implementing a queue class that ensures Promise execution in mutual exclusion, which I named PromiseQueue:

class PromiseQueue {
    constructor() {
        this._queue = new Array(); // Alternatively, a LinkedList for improved performance
        this._usingQueue = false;
    }

    /**
     * Adds an element to the queue and initiates its processing. It resolves upon successful promise execution.
     *
     * @param {Promise<any>} promise
     */
    add(promise) {
        const self = this;
        return new Promise((resolve, reject) => {
            const promiseData = {
                promise,
                resolve,
                reject,
            };
            self._queue.push(promiseData);
            self._runQueue();
        });
    }

    async _runQueue() {
        if (!this._usingQueue && this._queue.length > 0) {
            this._usingQueue = true;
            const nextPromiseData = this._queue.shift();
            const { promise, resolve, reject } = nextPromiseData;
            try {
                const result = await promise();
                resolve(result);
            } catch (e) {
                reject(e);
            }
            this._usingQueue = false;
            this._runQueue();
        }
    }
}

Utilization of this class would resemble the following (no testing conducted):

const myPromiseQueue = new PromiseQueue();

// By leveraging this structure, the second post is guaranteed to execute only after completion of the first.
myPromiseQueue.add(async() => await post(1000, localState));
myPromiseQueue.add(async() => await post(3000, localState));

Answer №3

I do not have any previous experience with TypeScript, so the responsibility of conversion falls upon you.

One suggestion would be to implement a queue method in your state object that takes a callback as its parameter. If the callback function returns a promise, the queue will wait for it to resolve before executing the next item. Otherwise, it will move on to the next item immediately.

function createQueue() {
  var promise = Promise.resolve();
  return function (fn) {
    promise = promise.then(() => fn(this));
    return promise;
  };
}

const localState = { state: 1000, queue: createQueue() };

const timeout = (...args) => new Promise(resolve => setTimeout(resolve, ...args));
const promiseTimeout = (time, value) => timeout(time, value + time);

const post = (time, state) => state.queue(() => {
  return promiseTimeout(time, state.state).then(result => {
    state.state = result;
    console.log(state.state);
  });
});

post(1000, localState).then(() => console.log("post 1000 complete"));
post(3000, localState).then(() => console.log("post 3000 complete"));

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Executing a component's function from a JavaScript file

Is it possible in React to call a function or method of a component from a separate JS file in order to modify the component's state? Here are three example files: First, App.js import React,{Component} from 'react'; import Login from &ap ...

What could be the reason for the inconsistent behavior of onClick, causing it to occasionally fail to display the associated

I just started using typescript 2 days ago, mainly to create a custom component for my streamlit app. I've made a navigation bar with a tab that can be clicked on the sidebar, but it's displaying some erratic behavior. Sometimes when I click on t ...

Modifying the chart width in Chart.js: A step-by-step guide

After creating a chart using Chart Js, I encountered an issue where the chart did not fit within the specified width. Adjusting the attributes of canvas proved to be ineffective, specifically with regards to the width attribute. Despite changing the value, ...

Transfer sound data blob to server and store it as a file

Currently, I have 3 buttons on my webpage - one to start recording audio, one to stop the recording, and one to play the recorded audio. Using MediaRecorder makes it easy to capture audio as a blob data. However, I am facing issues when trying to upload th ...

Tips for displaying specific HTML elements in AngularJS using ng-repeat and ng-if

I am working with some bootstrap HTML code that includes an ng-repeat function. <div class="row"> <div class="col-lg-4" ng-repeat="p in persons"> <img class="img-circle" src="images/{{ p.image }}" width="140" height="140"> < ...

Deliver varied asset routes depending on express js

I have a situation where I am working with express routes for different brands. My goal is to serve two separate asset directories for each brand route. One directory will be common for all brand routes, located at public/static, and the other will be spec ...

Using jQuery, remove any white spaces in a textbox that are copied and pasted

There is a textbox for entering order IDs, consisting of 7 digits. Often, when copying and pasting from an email, extra white spaces are unintentionally included leading to validation errors. I am looking for a jQuery script to be implemented in my Layout ...

jQuery Form Validator: requiring double submission

I've been working on implementing the Form Validator using Jquery. After some testing, I noticed that I have to click the submit button twice before the validation kicks in. Why is that? The script is housed in a separate file and included in my proj ...

Can you show me the method to retrieve the value of client.query in Node JS using PG?

I have been working with node.js to establish a database connection with postgresql. Here is what my dbConfig.js file looks like: var pg = require('pg'); var client = new pg.Client({ host:'myhoost', port:'5432', ...

What steps should I take to create this animation?

So here's my concept: I envision a circle div positioned in the center with multiple small lines extending outwards, resembling rays of sunlight. How should I go about achieving this effect? Would implementing JavaScript or utilizing CSS3 animations b ...

What is the best method for saving HTML form data into a Node JS variable?

I am facing an issue with setting the values of HTML form inputs onto my Node JS variables. In the JavaScript code below, I am struggling to assign values to the variables "hostname" and "port," which should then be concatenated to create a new variable ca ...

The button fails to log any text to the developer console

Attempting to verify the functionality of my button by logging a message on the developer console. However, upon clicking the button, the text does not appear in the console. import { Component, EventEmitter, Input, Output } from '@angular/core'; ...

Issue creating a JWT using jsonwebtoken module in Node.js

Could someone help me troubleshoot an issue I'm having while trying to generate a JWT token? The error message "TypeError: Right-hand side of 'instanceof' is not an object" keeps appearing even though the syntax seems correct. const jsonwebt ...

What is preventing jQuery UI from detecting jQuery?

I have successfully created a JavaScript widget that is capable of being embedded on any third-party website, in any environment. The widget relies on jQuery and jQuery UI for its functionality. After following the guidelines outlined in How to embed Javas ...

"Enhance your audio experience across all browsers with the revolutionary

I am looking for a way to play an mp3 file endlessly when the user opens my website. I need a lightweight crossbrowser solution that works in all browsers, including IE. Any suggestions? ...

"Upon querying with an AJAX POST request, the return value was

I'm encountering an issue where I am attempting to send data from JavaScript to PHP, make a database request, and have the result returned to my JS. However, all I get is "null" as a result. I have verified that my query is correct. Here is the JS cod ...

Issue with Custom Directive: Receiving Token Error for Expression in Isolated Scope in Angular

(The following messages, code snippets, etc, have been slightly altered for clarification) The error message received is as follows: Syntax Error: Token 'promiseObject' is unexpected, expecting [:] at column 3 of the expression [{{promiseObject ...

Customized slider in jQuery UI allowing users to select height using a scaled ruler

Currently, I am tackling a challenging math problem related to adjusting the height selector input. Essentially, I have implemented a jQuery UI slider for choosing a height. It operates in inches and ranges from 0 to 120 (equivalent to 10 feet in height). ...

ClickAwayListener's callback function stops executing midway

I am currently utilizing Material-UI's ClickAwayListener in conjunction with react-router for my application. The issue I have come across involves the callback function of the ClickAwayListener being interrupted midway to allow a useEffect to run, on ...

Running a function following several iterations of angular.forEach

let values1 = [1, 2, 3]; angular.forEach(values1, function(value){ $compile(value)($scope); }); let values2 = ['a', 'b', 'c']; angular.forEach(values2, function(value){ $compile(value)($scope ...