I want to fetch from the backend several data and process them at the end when they're loaded. Right now, when this code runs, data1 and data2 are undefined. I want to wait for them but I don't know how to keep my code clean. I'm not sure Promise.all() will fit here because I need to save the value of data1 and data2 specificaly and I cannot write general code to resolve for a Promise.all().
return new Promise( (resolve,reject) => {
let data1;
let data2;
let url1 = "http://fakeapi.com/getData1";
fetch(url1)
.then(res => res.json())
.then(data => data1SpecificAction())
.then(data => data1 = data)
.catch("cannot fetch data1")
let url2 = "http://fakeapi.com/getData2";
fetch(url2)
.then(res => res.json())
.then(data => data2 = data)
.catch("cannot fetch data2")
if(data1 && data2) {
resolve()
}
else {
reject()
}
}
How can I fix this snippet?
You don't need a lot of what you have in your question or your answer. You can just do this:
const url1 = "http://fakeapi.com/getData1";
const url2 = "http://fakeapi.com/getData2";
return Promise.all([
fetch(url1).then(res => res.json()).then(data1SpecificAction),
fetch(url2).then(res => res.json())
]).then([data1, data2] => {
if (data1 && data2) return; // resolve
// reject with appropriate error
let msg = data1 ? "missing data2" : "missing data1";
throw new Error(msg);
});
Things you do not need to do in your question and your answer:
Don't wrap existing promises in another manually created promise. That is considered an anti-pattern as the manually created promise is simply not needed. You can just return the promises you already have.
Don't assign a .then() result to a higher scoped variable. Though there are occasionally reasons for this, this is not one of them and is usually a warning sign that you're doing things wrong.
There are some annoyances about the fetch() interface that I find often benefit from a helper function (such as a 404 status resolves, not rejects):
function fetchJson(...args) {
return fetch(...args).then(res => {
if (!res.ok) throw new Error(`Got ${res.status} status`);
return res.json();
});
}
const url1 = "http://fakeapi.com/getData1";
const url2 = "http://fakeapi.com/getData2";
return Promise.all([
fetchJson(url1).then(data1SpecificAction),
fetchJson(url2)
]).then([data1, data2] => {
if (data1 && data2) return; // resolve
// reject with appropriate error
let msg = data1 ? "missing data2" : "missing data1";
throw new Error(msg);
});
And, in your specific case where you want to reject if the result is falsey, you could even do this:
function fetchJsonCheck(...args) {
return fetch(...args).then(res => {
if (!res.ok) throw new Error(`Got ${res.status} status`);
return res.json();
}).then(result => {
if (!result) throw new Error("empty result");
return result;
});
}
const url1 = "http://fakeapi.com/getData1";
const url2 = "http://fakeapi.com/getData2";
return Promise.all([
fetchJsonCheck(url1).then(data1SpecificAction),
fetchJsonCheck(url2)
]);
The solution was to put my specific action into another promises and use promise.all
return new Promise( (resolve,reject) => {
let data1;
let data2;
let promise1 = new Promise( (resolve,reject) => {
let url1 = "http://fakeapi.com/getData1";
fetch(url1)
.then(res => res.json())
.then(data => data1SpecificAction())
.then(data => {
data1 = data;
resolve()
}
.catch(()=>reject("cannot fetch data1"))
})
let promise 2 = new Promise( (resolve,reject) => {
let url2 = "http://fakeapi.com/getData2";
fetch(url2)
.then(res => res.json())
.then(data => {
data2 = data;
resolve()
}
)
.catch(()=>reject("cannot fetch data2"))
})
Promise.all([promise1,promise2])
.then(
() => {
if(data1 && data2) {
resolve()
}
else {
reject()
}
}
)
here is a link explaining how to pass data between promises.
Related
I need to make two calls to Firebase (as it doesn't support OR queries) and merge the output into one array at the end to return to the calling service.
I have something that gets pretty close but it outputs a 2D array of arrays (one for each call to Firebase). I've tried a few things and this is the best I can get to. Any help on tidying up the below would be great.
getAllFriends(): Observable<[Friendship[], Friendship[]]> {
const invitesSent = from(this.afAuth.currentUser.then(user => {
return user.uid;
}))
.pipe(
switchMap(
userid => {
return this.db.collection('friendships', ref => ref.where('inviter', '==', userid)).snapshotChanges().pipe(map(actions => {
return actions.map(action => {
const data = new Friendship(action.payload.doc.data());
data.id = action.payload.doc.id;
console.log(data);
return data;
});
}));
}
)
);
const invitesReceived = from(this.afAuth.currentUser.then(user => {
return user.uid;
}))
.pipe(
switchMap(
userid => {
return this.db.collection('friendships', ref => ref.where('invitee', '==', userid)).snapshotChanges().pipe(map(actions => {
return actions.map(action => {
const data = new Friendship(action.payload.doc.data());
data.id = action.payload.doc.id;
console.log(data);
return data;
});
}));
}
)
);
return combineLatest([invitesSent, invitesReceived]);
}
Friendship is just an object with property: value pairs, nothing special.
I have tried then putting a .pipe() after this returned observable but that just stops the subscription firing in the calling service.
What about returning, at the end, something like this
return combineLatest([invitesSent, invitesReceived]).pipe(
map(([frienships_1, friendships_2]) => ([...friedships_1, ...friendships_2]))
)
I am learning to use RXJS. In this scenario, I am chaining a few async requests using rxjs. At the last mergeMap, I'd like to have access to the first mergeMap's params. I have explored the option using Global or withLatest, but neither options seem to be the right fit here.
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => {
return readCSVFile(gauge.id);
}),
mergeMap((csvStr: any) => readStringToArray(csvStr.data)),
map((array: string[][]) => transposeArray(array)),
mergeMap((array: number[][]) => forkJoin(uploadToDB(array, gauge.id))),
catchError(error => of(`Bad Promise: ${error}`))
);
readCSVFile is an async request which returns an observable to read CSV from a remote server.
readStringToArray is another async request which returns an observable to convert string to Arrays
transposeArray just does the transpose
uploadToDB is async DB request, which needs gague.id from the first mergeMap.
How do I get that? It would be great to take some advice on why the way I am doing it is bad.
For now, I am just passing the ID layer by layer, but it doesn't feel to be correct.
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => readCSVFile(gauge.id)),
mergeMap(({ data, gaugeId }: any) => readStringToArray(data, gaugeId)),
map(({ data, gaugeId }) => transposeArray(data, gaugeId)),
mergeMap(({ data, gaugeId }) => uploadToDB(data, gaugeId)),
catchError(error => of(`Bad Promise: ${error}`))
);
Why don't you do simply this?
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => readCSVFile(gauge.id).pipe(
mergeMap((csvStr: any) => readStringToArray(csvStr.data)),
map((array: string[][]) => transposeArray(array)),
mergeMap((array: number[][]) => forkJoin(uploadToDB(array, gauge.id)))
)),
catchError(error => of(`Bad Promise: ${error}`))
);
You can also wrap the inner observable in a function:
uploadCSVFilesFromGaugeID(gaugeID): Observable<void> {
return readCSVFile(gaugeID).pipe(
mergeMap((csvStr: any) => readStringToArray(csvStr.data)),
map((array: string[][]) => transposeArray(array)),
mergeMap((array: number[][]) => forkJoin(uploadToDB(array, gaugeID))
);
}
In order to do this at the end:
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => uploadCSVFileFromGaugeID(gauge.id)),
catchError(error => of(`Bad Promise: ${error}`))
);
MergeMap requires all observable inputs; else, previous values may be returned.
It is a difficult job to concatenate and display the merging response. But here is a straightforward example I made so you can have a better idea. How do we easily perform sophisticated merging.
async playWithBbservable() {
const observable1 = new Observable((subscriber) => {
subscriber.next(this.test1());
});
const observable2 = new Observable((subscriber) => {
subscriber.next(this.test2());
});
const observable3 = new Observable((subscriber) => {
setTimeout(() => {
subscriber.next(this.test3());
subscriber.complete();
}, 1000);
});
console.log('just before subscribe');
let result = observable1.pipe(
mergeMap((val: any) => {
return observable2.pipe(
mergeMap((val2: any) => {
return observable3.pipe(
map((val3: any) => {
console.log(`${val} ${val2} ${val3}`);
})
);
})
);
})
);
result.subscribe({
next(x) {
console.log('got value ' + x);
},
error(err) {
console.error('something wrong occurred: ' + err);
},
complete() {
console.log('done');
},
});
console.log('just after subscribe');
}
test1() {
return 'ABC';
}
test2() {
return 'PQR';
}
test3() {
return 'ZYX';
}
I am trying to make two ajax call one after the other , i.e with the result of the first call data , i am making the second call. I am trying to use thunk , but it is not happening , i am getting errors.
actions.js
const fetchedStateNameSucess = (json) => {
return {
type: 'FETCH_STATE_NAME_SUCCESS',
json
}
}
const fetchProvidersDetailsSuccess = (providersData) => {
return {
type: 'FETCH_PROVIDER_DETAILS_SUCCESS',
providersData
}
}
export const fetchProvidersDetails = (providerId) => {
return (dispatch) => {
const providerUrl = `http://someUrl`;
const stateClientCountUrl = `http://someUrl/${state}`;
fetch(providerUrl)
.then(response => response.json())
.then(json => dispatch(fetchProvidersDetailsSuccess(json)))
.then((stateValue = json[0].stateVal)
fetch(stateClientCountUrl)
dispatch(fetchedStateNameSucess(response)));
};
}
In the above call , fetch(providerUrl) , i am getting response where i am getting the stateval , how to use that in making the second call to fetch(stateClientCountUrl) which takes stateval as a parameter.
As Miguel said you can do your second query in .then() clause as well as you can use async/await syntax, something like this:
export const fetchProvidersDetails = providerId => {
return async dispatch => {
const providerUrl = `http://someUrl`;
try {
const response = await fetch(providerUrl);
const json = await response.json();
dispatch(fetchProvidersDetailsSuccess(json))
const stateClientCountUrl = `http://someUrl/${json[0].stateVal}`;
const response2 = await fetch(stateClientCountUrl);
const json2 = await response2.json();
dispatch(fetchedStateNameSucess(json2));
} catch (error) {
console.log('Error', error);
}
}
If you want to use values from the first call response for the second fetch call, you need to do the second fetch after the first one has succeeded, more or less like this:
export const fetchProvidersDetails = (providerId) => {
return (dispatch) => {
const providerUrl = `http://someUrl`;
const stateClientCountUrl = `http://someUrl/${state}`;
fetch(providerUrl)
.then(response => response.json())
.then(json => {
dispatch(fetchProvidersDetailsSuccess(json));
const stateValue = json[0].stateVal;
fetch(stateClientCountUrl)
.then(response => dispatch(fetchProvidersDetailsSuccess(response)));
})
}
Don't forget to add error handling there, both for HTTP status codes and for JavaScript errors (by adding corresponding catch clauses).
I'm trying to write a test that will run a GET over all items. To do this, I get that list in the before block, then I want to have an it block for each item. I am trying to do this by putting the it block inside itemList.forEach. However, I suspect that the problem here is that the blocks never get registered for the test. How can I run this test as desired?
let token;
let itemList;
describe('GET items/:itemId with Admin', async () => {
before(async () => {
// NOTE: item.find({}) returns a promise of a list of all items
itemList = await item.find({});
console.log(item[0]._id) // this logs correctly!
const res = await userLogin(admin);
token = res.body.accessToken.toString();
});
it('registers initial it test', () => {
// This test passes and logs the statement
console.log('first test registered')
console.log(itemList.length) // successfully logs non-zero value
})
await itemList.forEach(async (item) => {
it('respond with json with a item', () => {
const itemId = item._id;
return getItem(itemId, token)
.then((response) => {
assert.property(response.body, '_id');
});
});
});
});
Afaik the before setup runs before every it test. It doesn't run immediately, and definitely does not wait for anything until you try to iterate your itemList. I think you will need to do either
describe('GET items/:itemId with Admin', async () => {
let token;
before(async() => {
const res = await userLogin(admin);
token = res.body.accessToken.toString();
});
// a list of all items for which tests should be created
const itemList = await item.find({});
console.log(itemList.length) // successfully logs non-zero value
for (const item of itemList) {
it('responds with json for item '+item, () => {
const itemId = item._id;
return getItem(itemId, token).then((response) => {
assert.property(response.body, '_id');
});
}
});
or
describe('GET items/:itemId with Admin', () => {
let itemList;
let token;
before(async() => {
[itemList, token] = await Promise.all([
item.find({}),
userLogin(admin).then(res => res.body.accessToken.toString())
]);
});
it('responds with json for every item', () => {
return Promise.all(itemList.map(item => {
const itemId = item._id;
return getItem(itemId, token)
.then((response) => {
assert.property(response.body, '_id');
});
});
}));
});
});
This is the solution I ended up with. I ended up putting a new describe block in the before block. The before block results the promise that gives the list of items. There is an it block in the top level so that mocha registers the test in the first place.
describe('GET items/:itemId with Admin', async () => {
before((done) => {
Item.find({}).then(async (itemList) => {
// create the admin user to get the items with
await createUsers([admin]);
const res = await userLogin(admin);
const token = res.body.accessToken.toString();
itemList.forEach((item, index) => {
const itemId = item._id;
describe(`get item number ${index}: _id: ${itemId}`, () => {
it('responds with item id', () =>
getItem(item, token)
.expect(200)
.then((response) => {
assert.notProperty(response.body, 'error');
assert.property(response.body, '_id');
assert.equal(response.body._id, itemId);
}));
});
});
done();
});
});
// If there is no it block here, it will not run the before block!
it(`register the initial it`, () => {
assert.equal('regression test!', 'regression test!');
});
});
Observable.bindCallback only returns value if I subscribe to it directly
in other words, this works fine:
return this.store.select(store => store.appDb.appBaseUrl)
.take(1)
.mergeMap(baseUrl => {
const url = `${baseUrl}?command=GetCustomers&resellerUserName=aaa&resellerPassword=bbbb`;
return this.http.get(url)
.map(res => {
var xmlData = res.text()
const boundCallback = Observable.bindCallback(this.processXml, (xmlData: any) => xmlData);
return boundCallback(this, xmlData)
.subscribe((x) => {
return x;
});
})
})
however I need to avoid the subscription as I am running inside #effect which auto subscribes for me, so I run:
return this.store.select(store => store.appDb.appBaseUrl)
.take(1)
.mergeMap(baseUrl => {
const url = `${baseUrl}?command=GetCustomers&resellerUserName=aaa&resellerPassword=aaa`;
return this.http.get(url)
.map(res => {
var xmlData = res.text()
const boundCallback = Observable.bindCallback(this.processXml, (xmlData: any) => xmlData);
var d:any = boundCallback(this, xmlData)
return d;
}).map(d=>{console.log(d)})
})
but instead of getting a value now I am getting a:
and this is d:
regards
Sean
If I understand what you want to do it should look something like this (obviously I didn't test it):
return this.store.select(store => store.appDb.appBaseUrl)
.take(1)
.mergeMap(baseUrl => {
const url = `${baseUrl}?command=GetCustomers&resellerUserName=aaa&resellerPassword=aaa`;
return this.http.get(url)
.mergeMap(res => {
var xmlData = res.text()
const boundCallback = Observable.bindCallback(this.processXml, (xmlData: any) => xmlData);
return boundCallback(this, xmlData)
}).do(d => console.log(d))
})
I used mergeMap() because I want to get the value from the Observable returned by boundCallback().
Also when using map() you always need to return a value that is propagated further. In your example you're not returning anything so you can use just do() to see print what values go through.
Edit:
So this is a simplified version of what you're trying to do.
class A {
private processXml(context, xmlData, cb) {
context.parseString(xmlData, {attrkey: 'attr'}, function (err, result) {
if (err || !result) return cb(null); return cb(result);
})
}
private parseString(str, _, cb) {
return cb(null, str);
}
private mockHttpGet() {
return Rx.Observable.of({
text: () => {
return 'abc';
}
});
}
test() {
return this.mockHttpGet()
.mergeMap(res => {
var xmlData = res.text();
const boundCallback = Rx.Observable.bindCallback(this.processXml, (xmlData: any) => xmlData);
return boundCallback(this, xmlData)
}).do(d => console.log(d))
}
}
let a = new A();
a.test().subscribe(val => console.log('subscribed result', val));
See live demo: https://jsbin.com/reweraw/2/edit?js,console
This demo prints:
abc
subscribe abc
The BoundCallbackObservable (and this applies to operators as well) do nothing until you subscribe to them. That's why in the debugger you see just raw data.
My demo works as you probably want so check out how am I using mergeMap() to get the actual value from the nested Observable and try to replicate the same logic in you application.
found the solution, I was one Obs too deep:
return this.store.select(store => store.appDb.appBaseUrl)
.take(1)
.mergeMap(baseUrl => {
const url = `${baseUrl}?command=GetCustomers&resellerUserName=reseller#ms.com&resellerPassword=XXXX`;
return this.http.get(url)
.map(res => {
return res.text()
}).flatMap(jData=>{
const boundCallback = Observable.bindCallback(this.processXml, (xmlData: any) => xmlData);
return boundCallback(this, jData)
}).map(businessData=>{
return businessData;
})
})