Continue on error in RxJs pipeable with mergeMap - javascript

I am doing some parallel HTTP get with RxJs pipe and the mergeMap operator.
On the first request fail (let's imagine /urlnotexists throw a 404 error) it stops all other requests.
I want it to continue query all remaining urls without calling all remaining mergeMap for this failed request.
I tried to play with throwError, and catchError from RxJs but without success.
index.js
const { from } = require('rxjs');
const { mergeMap, scan } = require('rxjs/operators');
const request = {
get: url => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url === '/urlnotexists') { return reject(new Error(url)); }
return resolve(url);
}, 1000);
});
}
};
(async function() {
await from([
'/urlexists',
'/urlnotexists',
'/urlexists2',
'/urlexists3',
])
.pipe(
mergeMap(async url => {
try {
console.log('mergeMap 1:', url);
const val = await request.get(url);
return val;
} catch(err) {
console.log('err:', err.message);
// a throw here prevent all remaining request.get() to be tried
}
}),
mergeMap(async val => {
// should not pass here if previous request.get() failed
console.log('mergeMap 2:', val);
return val;
}),
scan((acc, val) => {
// should not pass here if previous request.get() failed
acc.push(val);
return acc;
}, []),
)
.toPromise()
.then(merged => {
// should have merged /urlexists, /urlexists2 and /urlexists3
// even if /urlnotexists failed
console.log('merged:', merged);
})
.catch(err => {
console.log('catched err:', err);
});
})();
$ node index.js
mergeMap 1: /urlexists
mergeMap 1: /urlnotexists
mergeMap 1: /urlexists2
mergeMap 1: /urlexists3
err: /urlnotexists
mergeMap 2: /urlexists
mergeMap 2: undefined <- I didn't wanted this mergeMap to have been called
mergeMap 2: /urlexists2
mergeMap 2: /urlexists3
merged: [ '/urlexists', undefined, '/urlexists2', '/urlexists3' ]
I expect to make concurrent GET requests and reduce their respectives values in one object at the end.
But if some error occurs I want them not to interrupt my pipe, but to log them.
Any advice ?

If you want to use RxJS you should add error handling with catchError and any additional tasks to a single request before you execute all your requests concurrently with forkJoin.
const { of, from, forkJoin } = rxjs;
const { catchError, tap } = rxjs.operators;
// your promise factory, unchanged (just shorter)
const request = {
get: url => {
return new Promise((resolve, reject) => setTimeout(
() => url === '/urlnotexists' ? reject(new Error(url)) : resolve(url), 1000
));
}
};
// a single rxjs request with error handling
const fetch$ = url => {
console.log('before:', url);
return from(request.get(url)).pipe(
// add any additional operator that should be executed for each request here
tap(val => console.log('after:', val)),
catchError(error => {
console.log('err:', error.message);
return of(undefined);
})
);
};
// concurrently executed rxjs requests
forkJoin(["/urlexists", "/urlnotexists", "/urlexists2", "/urlexists3"].map(fetch$))
.subscribe(merged => console.log("merged:", merged));
<script src="https://unpkg.com/#reactivex/rxjs#6.5.3/dist/global/rxjs.umd.js"></script>

If you are willing to forego RXJS and just solve with async/await it is very straightforward:
const urls = ['/urlexists', '/urlnotexists', '/urlexists2', '/urlexists3'];
const promises = urls.map(url => request(url));
const resolved = await Promise.allSettled(promises);
// print out errors
resolved.forEach((r, i) => {
if (r.status === 'rejected') {
console.log(`${urls[i]} failed: ${r.reason}`)
}
});
// get the success results
const merged = resolved.filter(r => r.status === 'resolved').map(r => r.value);
console.log('merged', merged);
This make use of Promise.allSettled proposed helper method. If your environment does not have this method, you can implement it as shown in this answer.

Related

Promise.allSettled - set time delay to avoid API Abuse Rate Limits

I'm working with Promise.allSettled to do multiple fetches to the Github API for a few hundred requests. I'm getting blocked as they're all getting called at once, so I want to add a delay of 5ms between each. Is this possible with Promise.allSettled?
allSettled will neither help nor hinder you; it has no logic at all with regard to timing, other than to wait for the promises to settle. It will be up to you to create your promises so that they have the delays you want.
For example:
const delay = (milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds);
const urls = ['www.foo.com/whatever', /* etc for a few hundred urls */];
const promises = urls.map((url, index) => {
return delay(index * 5)
.then(() => fetch(url))
})
Promise.allSettled(promises)
.then(results => {
// do whatever with the results
});
Probably you need a concurrency limit, not a delay.
Fetch with concurrency limit:
Plain JS demo
const urls = [
"https://run.mocky.io/v3/40594989-00c2-4798-94da-c4a9ab251558?mocky-delay=3s&foo1",
"https://run.mocky.io/v3/40594989-00c2-4798-94da-c4a9ab251558?mocky-delay=3s&foo2",
"https://run.mocky.io/v3/40594989-00c2-4798-94da-c4a9ab251558?mocky-delay=3s&foo3",
"https://run.mocky.io/v3/40594989-00c2-4798-94da-c4a9ab251558?mocky-delay=3s&foo4",
"https://run.mocky.io/v3/40594989-00c2-4798-94da-c4a9ab251558?mocky-delay=3s&foo5",
"https://run.mocky.io/v3/40594989-00c2-4798-94da-c4a9ab251558?mocky-delay=3s&foo6",
"https://run.mocky.io/v3/40594989-00c2-4798-94da-c4a9ab251558?mocky-delay=3s&foo7"
];
function fetchBottleneck(concurrency = 1) {
const queue = [];
let pending = 0;
return async (...args) => {
if (pending === concurrency) {
await new Promise((resolve) => queue.push(resolve));
}
pending++;
return fetch(...args).then((value) => {
pending--;
queue.length && queue.shift()();
return value;
});
};
}
// Use fetch queue
(async () => {
const fetchInQueue = fetchBottleneck(3);
//No matter how many times you call the 'fetchInQueue' method, you get a maximum of 3 pending requests at the same time.
const results = await Promise.all(
urls.map(async (url) => {
try {
const response = await fetchInQueue(url);
console.log(`Got response for ${url}`);
return { status: "fulfilled", value: await response.json() };
} catch (reason) {
return { status: "rejected", reason };
}
})
);
console.log("Done:", JSON.stringify(results));
})();
Better solution is to use concurrency limitation provided by several third-party libs (e.g. http://bluebirdjs.com/docs/api/promise.map.html)
Live browser example
import CPromise from "c-promise2";
CPromise.allSettled(
[
"https://run.mocky.io/v3/40594989-00c2-4798-94da-c4a9ab251558?mocky-delay=3s&foo1",
"https://run.mocky.io/v3/40594989-00c2-4798-94da-c4a9ab251558?mocky-delay=4s&foo2",
"https://run.mocky.io/v3/40594989-00c2-4798-94da-c4a9ab251558?mocky-delay=2s&foo3",
"https://run.mocky.io/v3/40594989-00c2-4798-94da-c4a9ab251558?mocky-delay=3s&foo4",
"https://run.mocky.io/v3/40594989-00c2-4798-94da-c4a9ab251558?mocky-delay=4s&foo5",
"https://fake.url.to.test/",
"https://run.mocky.io/v3/40594989-00c2-4798-94da-c4a9ab251558?mocky-delay=3s&foo6",
"https://run.mocky.io/v3/40594989-00c2-4798-94da-c4a9ab251558?mocky-delay=3s&foo7"
],
{
mapper: (url) => {
console.log(`request [${url}]`);
return fetch(url).then((response) => response.json());
},
concurrency: 2
}
).then((results) => {
console.log(`Done: `, JSON.stringify(results));
});

Batching React updates across microtasks?

I have code that looks something like:
// File1
async function fetchData() {
const data = await fetch(...);
setState({ data });
return data;
}
// File2
useEffect(() => {
(async () => {
const data = await fetchData();
setState({ data });
})();
});
This triggers 2 React commits in 1 task. This makes my app less than 60FPS. Ideally, I'd like to batch the 2 setStates. Currently, it looks like this:
Pink represents React commits (DOM operations). The browser doesn't have a chance to repaint until the second commit is done. I can give the browser a chance to repaint by adding await new Promise(succ => setTimeout(succ, 0)); between the setStates, but it'll be better if I could batch the commits.
It's also pretty much impossible to refactor this, since the useState exists in separate files.
I tried unstable_batchedUpdates but it doesn't work with async.
You can group fetchData, when fetchData is called with the same argument the cache is checked for a promise and that promise is returned instead of creating a new one (make a new fetch).
When the promise resolves then that cache entry is removed so when component mounts again it will fetch again. To change this behaviour you can pass a different cache object to the group funciton.
//group function (will always return promise)
const createGroup = (cache) => (
fn,
getKey = (...x) => JSON.stringify(x)
) => (...args) => {
const key = getKey(args);
let result = cache.get(key);
if (result) {
return result;
}
//no cache
result = Promise.resolve(fn.apply(null, args)).then(
(r) => {
cache.resolved(key); //tell cache promise is done
return r;
},
(e) => {
cache.resolve(key); //tell cache promise is done
return Promise.reject(e);
}
);
cache.set(key, result);
return result;
};
//cache that removes cache entry after resolve
const createCache = (cache = new Map()) => {
return {
get: (key) => cache.get(key),
set: (key, value) => cache.set(key, value),
//remove cache key when resolved
resolved: (key) => cache.delete(key),
//to keep cache:
//resolved: () => 'NO_OP',
};
};
//fetch data function
const fetchData = (...args) => {
console.log('fetch data called with', args);
return new Promise((resolve) =>
setTimeout(() => resolve(args), 1000)
);
};
//grouped fetch data
const groupedFetchData = createGroup(createCache())(
fetchData
);
groupedFetchData(1, 2, 3).then((resolve) =>
console.log('resolved with:', resolve)
);
groupedFetchData(1, 2, 3).then((resolve) =>
console.log('resolved with:', resolve)
);
I think you should be able to so something along the lines of this, the aim being to cache the calls for a certain amount of time and then pass them all to unstable_batchedUpdates at once.
import { unstable_batchedUpdates } from 'reactDOM'
import raf from 'raf'
const cache = []
let rafId = null
function setBatchedState(setState, data) {
cache.push({ setState, data })
if(!rafId) {
rafId = raf(() => {
unstable_batchedUpdates(() => {
cache.forEach(({setState, data}) => setState(data))
})
rafId = null
cache = []
})
}
}
export default setBatchedState
This is using requestAnimationFrame to debounce the calls to unstable_batchedUpdates, you may prefer to use setTimeout depending on your use case.

Struggle with chaining of promises in react application

JavaScript, React - sending multiple simultaneous ajax calls struggling with promises. Basically I want to chain the calls, if one server call completes then only do next call, and collect the successful response of calls from endpoint /pqr made inside makeServerCalls.
import Promise from 'bluebird';
import request from 'superagent';
// sends a post request to server
const servercall2 = (args, response) => {
const promise = new Promise((resolve, reject) => {
const req = request
.post(`${baseUrl}/def`)
.send(args, response)
.setAuthHeaders();
req.endAsync()
.then((res) => resolve(res))
.catch((err) => reject(err));
});
return promise;
};
// sends a post request to server
const servercall1 = (args) => {
const promise = new Promise((resolve, reject) => {
const req = request
.post(`${baseUrl}/abc`)
.send(args)
.setAuthHeaders();
req.endAsync()
.then((res) => resolve({res}))
.catch((err) => reject(err));
});
return promise;
};
// function to send request to cgi server to execute actions from ui
async function makeServerCalls(args, length) {
// convert args to two dimensional array, chunks of given length [[1,2,3], [4,5,6,], [7,8]]
const batchedArgs = args.reduce((rows, key, index) => (index % length === 0 ? rows.push([key])
: rows[rows.length - 1].push(key)) && rows, []);
const responses = [];
for (const batchArgs of batchedArgs) {
responses.push(
// wait for a chunk to complete, before firing the next chunk of calls
await Promise.all(
***// Error, expected to return a value in arrow function???***
batchArgs.map((args) => {
const req = request
.get(`${baseUrl}/pqr`)
.query(args)
// I want to collect response from above req at the end of all calls.
req.endAsync()
.then((response) =>servercall2(args,response))
.then((res) => res);
})
)
);
}
// wait for all calls to finish
return Promise.all(responses);
}
export function execute(args) {
return (dispatch) => {
servercall1(args)
.then(makeServerCalls(args, 3))
.then((responses) => {
const serverresponses = [].concat(...responses);
console.log(serverresponses);
});
};
}
Error: expected to return a value in arrow function. What am I doing wrong here?
Is this a right chaining or it can be optimized?
What happens if some call fails in between?
You can use Async library for this. No need to re-invent the wheel.
There is a waterfall function that takes a list of functions that execute in series. You can pass result of function 1 to function 2 to function 3 and so on. Once complete waterfall executes, you get the result in callback. You can read more about it in the docs in the link above.

fetch retry request (on failure)

I'm using browser's native fetch API for network requests. Also I am using the whatwg-fetch polyfill for unsupported browsers.
However I need to retry in case the request fails. Now there is this npm package whatwg-fetch-retry I found, but they haven't explained how to use it in their docs. Can somebody help me with this or suggest me an alternative?
From the fetch docs :
fetch('/users')
.then(checkStatus)
.then(parseJSON)
.then(function(data) {
console.log('succeeded', data)
}).catch(function(error) {
console.log('request failed', error)
})
See that catch? Will trigger when fetch fails, you can fetch again there.
Have a look at the Promise API.
Implementation example:
function wait(delay){
return new Promise((resolve) => setTimeout(resolve, delay));
}
function fetchRetry(url, delay, tries, fetchOptions = {}) {
function onError(err){
triesLeft = tries - 1;
if(!triesLeft){
throw err;
}
return wait(delay).then(() => fetchRetry(url, delay, triesLeft, fetchOptions));
}
return fetch(url,fetchOptions).catch(onError);
}
Edit 1: as suggested by golopot, p-retry is a nice option.
Edit 2: simplified example code.
I recommend using some library for promise retry, for example p-retry.
Example:
const pRetry = require('p-retry')
const fetch = require('node-fetch')
async function fetchPage () {
const response = await fetch('https://stackoverflow.com')
// Abort retrying if the resource doesn't exist
if (response.status === 404) {
throw new pRetry.AbortError(response.statusText)
}
return response.blob()
}
;(async () => {
console.log(await pRetry(fetchPage, {retries: 5}))
})()
I don't like recursion unless is really necessary. And managing an exploding number of dependencies is also an issue. Here is another alternative in typescript. Which is easy to translate to javascript.
interface retryPromiseOptions<T> {
retryCatchIf?:(response:T) => boolean,
retryIf?:(response:T) => boolean,
retries?:number
}
function retryPromise<T>(promise:() => Promise<T>, options:retryPromiseOptions<T>) {
const { retryIf = (_:T) => false, retryCatchIf= (_:T) => true, retries = 1} = options
let _promise = promise();
for (var i = 1; i < retries; i++)
_promise = _promise.catch((value) => retryCatchIf(value) ? promise() : Promise.reject(value))
.then((value) => retryIf(value) ? promise() : Promise.reject(value));
return _promise;
}
And use it this way...
retryPromise(() => fetch(url),{
retryIf: (response:Response) => true, // you could check before trying again
retries: 5
}).then( ... my favorite things ... )
I wrote this for the fetch API on the browser. Which does not issue a reject on a 500. And did I did not implement a wait. But, more importantly, the code shows how to use composition with promises to avoid recursion.
Javascript version:
function retryPromise(promise, options) {
const { retryIf, retryCatchIf, retries } = { retryIf: () => false, retryCatchIf: () => true, retries: 1, ...options};
let _promise = promise();
for (var i = 1; i < retries; i++)
_promise = _promise.catch((value) => retryCatchIf(value) ? promise() : Promise.reject(value))
.then((value) => retryIf(value) ? promise() : Promise.reject(value));
return _promise;
}
Javascript usage:
retryPromise(() => fetch(url),{
retryIf: (response) => true, // you could check before trying again
retries: 5
}).then( ... my favorite things ... )
EDITS: Added js version, added retryCatchIf, fixed the loop start.
One can easily wrap fetch(...) in a loop and catch potential errors (fetch only rejects the returning promise on network errors and the alike):
const RETRY_COUNT = 5;
async function fetchRetry(...args) {
let count = RETRY_COUNT;
while(count > 0) {
try {
return await fetch(...args);
} catch(error) {
// logging ?
}
// logging / waiting?
count -= 1;
}
throw new Error(`Too many retries`);
}

Map data from array into Promises and execute code on Promise.all() not working

I have an array like this
let array =[ {message:'hello'}, {message:'http://about.com'}, {message:'http://facebook.com'}]
I want to loop through it and at each item, I make a request to server to get open graph data, then save the fetched data back to array. Expected result
array =[
{message:'hello'},
{message: {
url:'http://about.com', title:'about'
}
},
{message:{
url:'http://facebook.com', title:'facebook'
}
}
]
And I need to execute something else after the async fully complete. Below code is what I think it would be
let requests = array.map( (item) => {
return new Promise( (resolve) => {
if (item.message.is('link')) {
axios.get(getOpenGraphOfThisLink + item.message)
.then( result => {
item.message =result.data
// console.log(item.message)
// outputs were
//{url:'http://about.com', title:'about'}
//{url:'http://facebook.com', title:'facebook'}
resolve()
})
}
})
})
Promise.all(requests).then( (array) => {
// console.log (array)
// nothing output here
})
The promise.all() won't run. console.log(array) does not output anything.
I see three main issues with that code:
Critically, you're only sometimes resolving the promise you create in the map callback; if item.message.is('link') is false, you never do anything to resolve. Thus, the Promise.all promise will never resolve.
You're accepting array as an argument to your Promise.all then callback, but it won't be one (or rather, not a useful one).
You're not handling the possibility of a failure from the axios call.
And if we resolve #2 by pre-filtering the array, then there's a fourth issue that you're creating a promise when you already have one to work with.
Instead:
let array = /*...*/;
let requests = array.filter(item => item.message.is('link'))
.map(item => axios.get(getOpenGraphicOfThisLink + item.message)
.then(result => {
item.message = result.data;
return result;
})
);
Promise.all(requests).then(
() => {
// Handle success here, using `array`
},
error => {
// Handle error
}
);
Note how reusing the axios promise automatically propagates the error up the chain (because we don't provide a second callback to then or a catch callback).
Example (doesn't demonstrate errors from axios.get, but...):
// Apparently you add an "is" function to strings, so:
Object.defineProperty(String.prototype, "is", {
value(type) {
return type != "link" ? true : this.startsWith("http://");
}
});
// And something to stand in for axios.get
const axios = {
get(url) {
return new Promise(resolve => {
setTimeout(() => {
resolve({data: "Data for " + url});
}, 10);
});
}
};
// The code:
let array =[ {message:'hello'}, {message:'http://about.com'}, {message:'http://facebook.com'}]
let requests = array.filter(item => item.message.is('link'))
.map(item => axios.get(/*getOpenGraphicOfThisLink + */item.message)
.then(result => {
item.message = result.data;
return result;
})
);
Promise.all(requests).then(
() => {
// Handle success here, using `array`
console.log(array);
},
error => {
// Handle error
console.log(error);
}
);

Categories

Resources