Axios - multiple requests for the same resource - javascript

I'm creating an app where in one page, I have two components requesting the same http resource. In this case I'm using axios and here's one example:
axios.get('/api/shift/type')
.then(
(response) => {
self.shiftTypes = response.data;
success(response.data)
},
(response) => {
error(response)
}
);
The issue lies in them requesting it almost at the same time. If Component A makes the request at the same time as Component B, 2 request calls are made and they'll get the same data. Is there a way to check if axios currently has an unresolved promise and return the result to both components once the request is resolved?
Not sure if it helps but the app is being built using the vue framework
Thanks
EDIT: I tried storing the promise in memory but Component B never gets the response
getShiftTypes(success, error = this.handleError, force = false) {
if (this.shiftTypes && !force) {
return Promise.resolve(this.shiftTypes);
}
if (this.getShiftTypesPromise instanceof Promise && !force) { return this.getShiftTypesPromise; }
let self = this;
this.getShiftTypesPromise = axios.get('/api/shift/type')
.then(
(response) => {
self.shiftTypes = response.data;
self.getShiftTypesPromise = null;
success(response.data)
},
(response) => {
error(response)
}
);
return this.getShiftTypesPromise;
}

Consider using a cache:
let types = { lastFetchedMs: 0, data: [] }
async function getTypes() {
const now = Date.now();
// If the cache is more than 10 seconds old
if(types.lastFetchedMs <= now - 10000) {
types.lastFetchedMs = now;
types.data = await axios.get('/api/shift/type');
}
return types.data;
}
while(types.data.length === 0) {
await getTypes();
}

Related

Vue 2.X - Good approach towards waiting for contrasting async data sources in a component

Within a Vue component I have two async calls for data from different sources (one a HTTP call, one a browser cache call) and I want to have a loader spin until both are complete. When both are complete the component is loaded so to speak.
...
data() {
return {
loading: true
}
},
mounted: function() {
this.getData(); // HTTP Async API
this.getOtherData(); // IndexedDB Async API
},
...
I'm trying to figure out the proper way to set loading to false when both return data. My current approach of using flags looks dirty but I'm not sure if its the best approach.
...
data() {
return {
loading: true,
flagA: false,
flagB: false
}
},
mounted: function() {
this.getData(); // HTTP Async API
this.getOtherData(); // IndexedDB Async API
},
methods: {
getData() {
...
//Once done
this.flagA = true
if (this.flagA == true && this.flagB == true) this.loading = false
},
getOtherData() {
...
//Once done
this.flagB = true
if (this.flagA == true && this.flagB == true) this.loading = false
}
...
Is there a better approach?
EDIT to show non HTTP function:
getOtherData() {
var request = indexedDB.open("Database", 1);
request.onerror = event => {
//IndexedDB is not supported. Resort to fallback option and continue regular program flow
};
request.onsuccess = event => {
this.db = event.target.result;
var transaction = this.db.transaction(["project"], "readwrite");
var projectStore = transaction.objectStore("project");
var projectReq = projectStore.get(1);
projectReq.onsuccess = () => {
this.flagB = true;
};
projectReq.onerror = () => {
//error
};
};
As long as both functions return a promise, use an async lifecycle hook and Promise.all. It works regardless of whether that promise comes from an http request or is just returned from a method:
async mounted() {
const p1 = axios.get('getData'); // not awaited, just returning promise
const p2 = this.getOtherData(); // not awaited, just returning promise
// awaiting both, responses will be in `response1` and `response2`
const [ response1, response2 ] = await Promise.all([p1, p2]);
this.loading = false;
}
Make sure that all functions return a promise if you don't use the http promise directly:
getData() {
...
return request;
},
getOtherData() {
...
return request;
}

Chaining the $http.get AngularJS

As I am making few call to the endpoints within a function and it was causing issue with the parallel calls to the endpoints, so it was suggested in the other question to use the promise chaining. I updated my code so we can call the endpoints one after the other, so the code looks like below
$scope.getRequest = function () {
var url = $rootScope.BaseURL;
var config = {
headers: {
'Authorization': `Basic ${$scope.key}`,
'Prefer': 'odata.maxpagesize=2000'
}
};
$http.get(url, config)
.then(newViewRequest)
.then(function(response){
$scope.viewRequest.data = response.data;
},
function (response) { // failure async
console.log("There was an error getting the request from CORE");});
};
var newViewRequest = function (response) {
var url1 = $rootScope.BaseURL + `CMQ_REQUEST('${$scope.viewRequest.barcode}')`;
if (response.data.REV_SAMPLE_CMQREQUEST.length = 0) {
return $http.get(url1, config)
}
return $q.reject({ message: 'Validations didnt work' });
};
It always sends the reject message back from the newViewRequest if response.data.REV_SAMPLE_CMQREQUEST.length = 0, if I comment it out I get the response.data is undefined.
Update your condition to validate instead of assigning
Issue: Update if condition as below to check response.data.REV_SAMPLE_CMQREQUEST.length whether it is 0 or not with === instead of =
if (response.data.REV_SAMPLE_CMQREQUEST.length === 0)

Retry failed API call to another origin in Angular

I want to implement a logic of retrying an API call to another origin if the call to current origin failed.
I want it to be handled in the service layer, for not to implement this logic in each component.
For example, I have such function in endpoint service
getAll(): Observable<IssueListItem[]> {
let endpointUrl = `${this.apiConnectionService.getApiUrl()}api/Issues`;
return this.http.get<IssueListItem[]>(endpointUrl, { headers: this.dataService.requestHeaders })
.pipe(retryOtherApi(this.apiConnectionService),
catchError(error => this.handleError(error)));
}
The consumer of this function looks like:
ngOnInit() {
this.issuesEndpointService.getAll()
.subscribe(_ => this.issues = _);
}
And I whant it know nothing about retry logic.
So, I tried to create an operator 'retryOtherApi' where I switch an origin to another one.
export function retryOtherApi(apiConnectionService: ApiConnectionService) {
const maxRetry = apiConnectionService.apiOriginsCount;
return (src: Observable<any>) => src.pipe(
retryWhen(_ => {
return interval().pipe(
flatMap(count => {
console.log('switch to: ' + apiConnectionService.getApiUrl())
apiConnectionService.switchToOther();
return count === maxRetry ? throwError("Giving up") : of(count);
})
);
})
);
}
Switching works, but unfortunately, the whole getAll function is not called and it retries n times with the same old URL.
So the question is how to implement common retry to other API logic if the current API become unavailable.
If to rephrase the question to the more common case, it becomes like how to recall HTTP endpoint with other params if the current one failed.
public getAll(): Observable<IssueListItem[]> {
let call = () => {
let endpointUrl = `${this.apiConnectionService.getApiUrl()}api/Issues`;
return this.http.get<IssueListItem[]>(endpointUrl, { headers: this.dataService.requestHeaders })
};
return <Observable<IssueListItem[]>>call()
.pipe(
retryOtherApi(this.apiConnectionService, () => call()),
catchError(error => this.handleError(error))
);
}
And here is a retry operator:
export function retryOtherApi(apiConnectionService: ApiConnectionService, recall: () => Observable<any>) {
const maxRetry = apiConnectionService.apiOriginsCount;
let count = 0;
let retryLogic = (src: Observable<any>) => src.pipe(
catchError(e => {
if (e.status === 0) {
apiConnectionService.switchToOther();
console.log('switch to: ' + apiConnectionService.getApiUrl())
return count++ === maxRetry ? throwError(e) : retryLogic(recall());
} else {
return throwError(e);
}
}));
return retryLogic;
}
It works, but:
Now I need to implement logic for other threads not to switch api
simultaneously.
Have some problems with TS typecasting, that is
why I hardcoded type before return.

Data initializing with promises

I really struggle with data initializing and promises. I am using Ionic 3 with Angular and Ionic storage, but my question is mainly related to how promises operate.
Basically I would like to achieve the following:
when my app starts it should use the local storage collection
if the local storage collection does not exist or is empty, create a new one with http
if the http fails, create a collection with local data.
My solution so far:
getNewsItems():Promise<any> {
return this.storage.get(this.newsKey).then((data) => {
if(data == null)
{
return (this.buildNewsItemsViaHttp());
} else {
return (data);
}
});
}
private buildNewsItemsViaHttp(){
return new Promise(resolve => {
this.http.get('some/url/to/fetch/data')
.subscribe(
data => {
this.newsCollection = data;
this.storage.set(this.newsKey, this.newsCollection);
resolve(this.newsCollection);
},
(err) => {
resolve (this.buildNewsItemsViaLocalJSON());
}
);
});
}
private buildNewsItemsViaLocalJSON() {
return new Promise(resolve => {
this.http.get('assets/data/newsCollectionLocal.json')
.subscribe(
data => {
this.newsCollection = data;
this.storage.set(this.newsKey, this.newsCollection);
resolve(this.newsCollection);
},
(err) => {
console.log(err);
}
);
});}
I don't like some parts of it, for example returning a promise inside a promise - is this actually an issue?
Thanks in advance
A clean solution could be using async\await methods:
async buildNewsItemsViaHttp(){
return await this.http.get()....
}
async buildNewsItemsViaLocalJSON(){
return await this.http.get()....
}
async getNewsItems(){
return await this.storage.get()...
}
async getItems(){
return ((await this.getNewsItems()) || (await this.buildNewsItemsViaLocalJSON()) || (await this.buildNewsItemsViaHttp()));
}
usage:
const items = await this.getItems();
You can optimize the resources, cache them and return them in each function.
Example:
async buildNewsItemsViaHttp(){
let result = cache.get(); // todo
if(!result){
result = await this.http.get()...
cache.set(result)
}
return result;
}

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`);
}

Categories

Resources