Using setInterval in promise chain which uses Redux-Thunk async action - javascript

I have a rather obscure problem and it may indeed arise from my own doing so feel free to point it out if that is the case.
I have a chain of promises, which I need access to intermediate values for subsequent API calls. Once the promises are all resolved, I then execute an API call that makes use of the various intermediate return values.
Finally, based on the results of that call I then make a final call to an API. However, the results of this call are used to update a Redux State and therefore are done using Redux Thunk as the middleware to facilitate async actions.
The challenge arises in that I then need to poll a database at a set interval to check if the Redux API call has carried out the work requested of it (This worked involved placing some long running tasks on a Redis task queue and then having the queue worker update a database once the task is complete).
When the database confirms that it has indeed been updated with the completed task status, I want to clear the setIntervalId. I have simplified the code to that shown below. The issue is that the execution does not wait for the Redux async action to be completed. So it executes the action then goes straight on to carry out the 'if' check before the Redux action is cmplete. Therefore the if is never true and the polling continues indefinitely.
I do not know how to ensure the Redux Action is complete before code continues. I have trimmed the code down to get the logic in place without overcrowding this post with irrelevant code.
Any ideas how I can get this to work as intended?
buttonClickedHandler.js
callbackFn {function(){
const a = ApiCallA(...) // Returns a promise
const b = a.then(resA => {
// Some work done here
return ApiCallB(…); // Returns a promise
const c = b.then(resB => {
// Some work done here
return ApiCallC(…); // Returns a promise
return Promise.all( ([a, b, c] )
.then( ([resA, resB, resC] ) => {
// Some work done here
return ApiCallD
})
.then( resD => {
const intId = setInterval( () => {
reduxAsyncAction(resD.jobId) // <-- This makes a final API call
if (props.jobStatus){ // <-- jobStatus gets updated too late
clearInterval(intId );
}
}, 5000)
})
.catch(err => console.log(err))
}
redux action creator
export const reduxAsyncAction= (jobId) => {
return dispatch => {
dispatch(taskStatusStart()); // <- Basic sync. action creator
return databaseQuery() // Returns Promise therefore pushed to taskqueue while other function run
.then(dbData=>{
const completionStatus = dbData.status; // <- True or False
dispatch(taskStatusSuccess(completionStatus)) // <- Basic sync. action creator
},
error => {
dispatch(taskStatusFail(error)); // <- Basic sync. action creator
})
};
}

Use componentDidUpdate or useEffect
These will run whenever props.jobStatus updates in your redux store
Check for taskStatusSuccess(completionStatus) or taskStatusFail(error) that you are dispatching from reduxAsyncAction
clearinterval(intId) if the status matches
You will also need to add the intId to be within scope by declaring outside of the callbackFn such as:
this.intId in the constructor of a class based component
let intId at the top of the functional component.
Class based components:
componentDidUpdate(){
if (intId && props.jobStatus === 'completionStatus' || props.jobStatus === 'error'){
clearInterval(intId);
intId = null;
}
}
Functional components:
useEffect(()=>{
if (intId && props.jobStatus === 'completionStatus' || props.jobStatus === 'error'){
clearInterval(intId);
intId = null;
}
},[props.jobStatus])

Related

Promise resolves earlier than expected and not returning an AXIOS call value

I'm making a request to a service that generates several strings in rapid succession. My problem arise from having to return a promise with this service, as I do not control the service process that returns the string.
I want to emphasize the fact that I necessarily need to return a promise.
Right now, what I have is the main function (handler.ts) and it doesn't mantain any business logic, including only the following:
public static callback: any;
public static async init(configuration, callback): Promise<any> {
this.callback = callback;
...
return new Promise((resolve, reject) => {
try {
const result = await Service.bootstrap(configuration)
return resolve(result)
} catch(err) {
reject(err)
}
}
}
This handler calls the service, which has the bootstrap function that performs a call to a .js file that obtains some information from my computer and then returns a string about it.
import loadComputerInformation from 'load.js'
public async bootstrap() : Promise<any> {
loadComputerInformation();
function useComputerInfoString() {
// window.getInfo is generated by loadComputerInformation(), and I can not control
// when does it return it, as it generates several strings with rapid succession
// and until the function exists, I will not get my string.
if (typeof window.getInfo !== 'function') {return;}
const data = window.getInfo();
if (data.finished) {
clearTimeout(timeoutId);
const infoString = data.computerInfo;
Service.axiosCall(infoString);
}
}
// I have to set an interval to perform the call several times, and until it resolves it
// it will not stop performing this call.
const timeoutId = setInterval(useComputerInfoString, 500);
}
return;
}
Therefore, the problem that I'm facing is that my promise gets lost in another thread, and I can not return the value from Service.axiosCall(infoString), which is just a standard axios call, but that necessarily needs the infoString.
Adding the axios call function just in case it is useful. Right now, I'm using the callback passed to the handler.js to return the axios call, but I want to be able to include it in the Promise without the necessity of a callback function
public static async axiosCall(blackbox): Promise<any> {
await axios.post('https://somecall.com/info', blackbox)
.then((response) => { this.callback(element)
return element);
}
}
Any idea of how to solve this?
Highlight
Please note that loadComputerInformation() asynchronously loads Window.getInfo(), but it does not resolve only one value, but several, therefore I can not await on Window.getInfo() because at the beggining it does not exist, and will only return undefined
Also, right now, the code is up and running, but the way it is returning the value is with the callback and not as a promise.
Try a bootstrap function whose returned promise resolves with the response returned from an axios call.
This suggestion (based on information in the post) is vanilla JavaScript to demonstrate how to the problem might be approached. Obviously modification to integrate it into the application will be needed:
const bootstrap = () => new Promise( resolve => {
loadComputerInformation();
let state = "load";
const timer = setInterval( ()=> {
if( state == "load") {
if( typeof window.getData != "function") {
return;
}
state = "get";
}
let data;
if( state == "get") {
data = window.getData();
if(!data.finished) {
return;
}
// state = "request";
}
clearInterval(timer);
resolve( axios('post', "https:example.com/info", data.computerInfo) );
}, 100); // 1/10th second?
};
Note this answer has been modified to use a state machine to wait for getData to be loaded asynchronously, then to wait for a call to getData to return a data object with a finished property, before resolving the promise returned with the promise returned by the axios call - the first answer was simpler right up until it needed to wait for getData code to be loaded asynchronously.
Beneath the question lies a problem that may have to be written off to code debt: JavaScript is event driven. loadComputerInformation appears to have been written for its results to be polled. It is hard to imagine a justifiable reason why it was left in that state.

Async in RxJS Observable

First time with RxJS. Basically, I'm trying to make a twitter scraper which retrieves tweets from a query string. The search url allows specifying of a min_position parameter which can be the last id of the previous search to sort of paginate.
The process kind of looks like this (where it loops back at the end):
get page -> next() each scraped tweet -> set min_position -> get page (until !has_more_items)
Requesting the page returns a promise and so I somehow have to wait until this is completed until I can proceed. I was hoping to pass an async function to Observable.create() but that doesn't seem to work, it's only called a single time.
EDIT
I've had a play around after reading your resources as best as I could. I came up with the following abstraction of my problem.
import { from, Observable } from 'rxjs'
import { concatMap, map, switchMap } from 'rxjs/operators'
let pageNumber = 0
const PAGE_SIZE = 3, MAX_PAGES = 3
async function nextPage() {
if (pageNumber >= MAX_PAGES) {
throw new Error('No more pages available')
}
await new Promise(res => setTimeout(res, 500)) // delay 500ms
const output = []
const base = pageNumber++ * PAGE_SIZE
for (let i = 0; i < PAGE_SIZE; i++) {
output.push(base + i)
}
return output
}
function parseTweet(tweet: number): string {
// simply prepend 'tweet' to the tweet
return 'tweet ' + tweet
}
const getTweets = (): Observable<string> => {
return from(nextPage()) // gets _html_ of next page
.pipe(
concatMap(page => page), // spreads out tweet strings in page
map(tweet => parseTweet(tweet)), // parses each tweet's html
switchMap(() => getTweets()) // concat to next page's tweets
// stop/finish observable when getTweets() observable returns an error
)
}
getTweets()
.subscribe(val => console.log(val))
It's quite close to working but now whenever nextPage() returns a rejected promise, the entire observable breaks (nothing logged to the console).
I've attempted inserting a catchError after the pipe to finish the observable instead of running through and throwing an error but I can't get it to work.
Also this implementation is recursive which I was hoping to avoid because it's not scalable. I don't know how many tweets/pages will be processed in the observable in future. It also seems that tweets from all 3 pages must be processed before the observable starts emitting values which of course is not how it should work.
Thanks for your help! :)
We need to loadTwits until some condition and somehow work with Promise? Take a look at example:
function loadTwits(id) {
// Observable that replay last value and have default one
twitId$ = new BehaviorSubject(id);
return twitId$.pipe(
// concatMap - outside stream emit in order inner do
// from - convert Promise to Observable
concatMap(id => from(fetchTwits(id))),
map(parseTwits),
// load more twits or comlete
tap(twits => getLastTwitId(twits) ? twitId$.next(getLastTwitId(twits)) : twitId$.complete())
)
}
I figured it out after looking further into expand and realising it was recursion that I needed in my observable. This is the code that creates the observable:
const nextPage$f = () => from(nextPage()) // gets _html_ of next page
.pipe(
concatMap(page => page), // spreads out tweet strings in page
map(tweet => parseTweet(tweet)) // parses each tweet's html
)
const tweets$ = nextPage$f()
.pipe(
expand(() => morePages() ? nextPage$f() : empty())
)

Determine if function is a jQuery promise

Suppose I have a page loader that can run a plain function, or a jquery AJAX request. I only want to move to the next step if the AJAX request was successful (ie done) and show an error if it wasn't (ie fail).
function runStep(stepText, functionToCall){
this.setStepText(stepText);
// call function
let result = functionToCall();
// if the function is jquery, wait for the result
if(result && typeof result.then === 'function'){
let self = this;
result.done(function(){
self.goToNextStep();
}).fail(function(){
self.showFailureMsg();
});
}
// if its a plain old function go to the next step
else {
this.goToNextStep();
}
}
What I don't get is; at the point I call done or faile, I've already run the jQuery AJAX request. So is it not too late to attach these handlers?
The nice thing about Promises is that it doesn't matter when you attach. Once they finalize, the state is constant.
In this way you can pass around a resolved or rejected Promise throughout the lifetime of your app and late listeners will still be called.
const pg = Promise.resolve("foo");
const pb = Promise.reject("bar");
pg.then(v => console.log(v));
pb.catch(err => console.error(err));
Since jqXHR Objects (which is what $.ajax() returns) implement the same interface as Promise, you can also attach whenever you want as well.
const d = $.Deferred();
const dpr = d.promise();
d.resolve("foo");
dpr.then(v => console.log(v));
setTimeout(() => {
console.log("Even 3 seconds later it's still foo");
dpr.then(v => console.log(v));
}, 3000);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Consider this concrete example that uses a real XHR:
const post = $.ajax("https://jsonplaceholder.typicode.com/posts/1");
post.then(p => {
// We are in the callback of the request
// so we know that it is complete
console.log(p.title);
// Let's add another callback when we *think*
// it should be too late
post.then(latep => {
// Still possible
console.log(p.title)
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

Calculate new state by old state and result of a promise

I have a project which does not support generators and async await syntax.
I built the following code with async await because I don't see any other way to do it:
this.setState(async lastState => {
const newImagesAmount = lastState.images.length + 20;
const newImages = await this.getImages(this.props.tag, newImagesAmount);
return {
images: newImages
};
});
Why? In this particular case, the new state is built by both the old state and a result of a promise.
How can I transform it to non - async await syntax?
Note (Update):
Due to the fact that both the current answers contain the same bug, please read #dhilt answer + responses first which explain what are the bugs.
First of all it's impossible to use await inside of setState(). Not because of your project's limitation but because each function with await inside should be declared as async beforehand. And it should be invoked with await notation or with .then() chained. So callback passed into .setState should be async and called with await but we know that React does not work this way. React expect ordinary function and call it without await or .then(). It looks like func.apply(context, funcArgs) in react-dom module. Also there is no check if it returns Promise or not.
So let's return back to your final goal. As far as I understand it's about ensuring state is still consistent after deferred data is loaded. And here is another limitation. Except sync XHR(that is really bad practice) there is no way to pause all the execution until data comes. That is: whatever you do you cannot be 100% sure state has not mutate between "request has been sent" and "data is received" steps. Neither await, nor functional setState does not allow freeze execution. Yes, your code is still referencing to "prevState" variable but it could be not actual component's state anymore!
So what you could do:
abort ongoing XHR before sending another one to the same endpoint(should work for read operations; does not make sense for write operations)
parse request params when processing response to inject it into state in valid way(say, first response is for 1..10 images and next one is for 11..20 - so you can work with that and inject second response into appropriate indexes leaving first 10 still unintialized)
set up some flag to block new requests until ongoing is finished
P.S. so making request from inside setState function leads to confusion and don't add any additional flexibility.
If the code in the question is correct and you just want to purge async/await in favour of traditional return promise.then(...) syntax, then do as follows:
this.setState(lastState => {
const newImagesAmount = lastState.images.length + 20;
return this.getImages(this.props.tag, newImagesAmount)
.then(newImages => ({ 'images': newImages });
});
The following simplification may also work :
this.setState(lastState => {
return this.getImages(this.props.tag, lastState.images.length + 20)
.then(images => ({ images });
});
Im assuming that lastState here is really just the current state before you update it. Try something like this:
const imageLength = this.state.images.length + 20
this.getImages(this.props.tag, imageLength)
.then(newImages => {
this.setState({images: newImages})
}
I think you may want something like this
const newImagesAmount = this.state.images.length + 20;
this.getImages(this.props.tag, newImagesAmount).then(newImages =>
this.setState(prevState => ({
..prevState,
images: newImages
})
);
Firstly, you do a request for new images and this request is based on current state. Then (when the result is arrived) you update state.
Updated version that allows to protect async logic from multiple executions before the request is finished
if (!this.state.pending) {
this.setState(lastState => {
const newImagesAmount = lastState.images.length + 20;
this.getImages(this.props.tag, newImagesAmount).then(newImages =>
this.setState(prevState => ({
...prevState,
pending: false,
images: newImages
})
).catch(() =>
this.setState(prevState => ({
...prevState,
pending: false
})
);
return {
...lastState,
pending: true
}
});
}

wait on a promise for each iteration in nodejs

I am developing a nodejs application that needs to get settings from an array(in a settings object), call a rest api based on the settings and write the response to mongodb and repeat this for the next setting in the array.
Here is a simplified version of the application
var setting //global
process(){ //top level function
for(let s of config.settings){
setting = s;
getData();
}
}
function getData(){
init()
.then(makeRequest) // constructs and makes the rest api call
.then(insert) // writes the response to the db
.catch(function(err){
// logs err
}
}
Running it, only the data for the last setting (in the array) is written to the db and this happens for each iteration. Basically the same data is written on the db for as many iterations.
The problem I can see from this is that the for loop finishes executing, before the promises return with the value.
I have seen some examples of async.for
Any suggestions on fixing this. How do you go about designing this kind of a flow?
You can bind the settings to each function call to preserve the value. looks like you'd have to refactor though as the value would be passed in as an argument though i'm not sure if your code is pseudo code or actual code.
async await would work as well but would take longer as it would pause execution at each api call.
You should return an object or array that you can use to store an internal state for your request. Please see the example for how it works.
Also never set a global variable to store your state, with your function being asynchronous the value may not be what you expect it to be.
With this approach you are passing { init } for the first promise, then { init, request } for the next so you have the response from each part of your promise chain that you can use to make further requests.
// return an object to store the state on init
const init = () =>
new Promise((res, rej) => res({
init: 'initted'
}))
// pass init and the request to the next function in the chain
const makeRequest = ({ init }) =>
new Promise((res, rej) => res({
init,
request: {
msg: 'this is the response',
id: 33
}
}))
// insert stuff from the request
// then return the data to the next query
const insert = ({ init, request }) =>
new Promise((res, rej) => res({
request,
init,
created_at: Date.now()
}))
const trace = name => x => (console.log(name, x), x)
function getData(){
return init() // return your promise so you can chain it further
.then(trace('after init'))
.then(makeRequest)
.then(trace('after request'))
.then(insert)
.then(trace('after insert'))
.catch(console.error)
}
// call you function
getData()
// since the promise is returned we can continue the chain
.then(state => console.log({ state }))
<script src="https://codepen.io/synthet1c/pen/KyQQmL.js"></script>
All of your loop will have executed by the time the callbacks are coming in. So settings will be the last value.
Instead of relying on globals, pass setting into getData, for example.

Categories

Resources