How to call make Angular Service synchronous? - javascript

I trying to read an item (menulist) from localStorage. If it is null, I am calling a service which will store menulist after fetching it from database).
It seems that service is asynchronous as I am getting Cannot read property 'sort' of null in the following code.
ngOnInit() {
this.menulist = localStorage.getItem('menulist');
if (!this.menulist) {
this.SetMenuList();
}
this.jsonmenulist = JSON.parse(this.menulist);
this.jsonmenulist.sort(function (a, b) {
return a.mnuposno > b.mnuposno;
});
this.jsonmenulist = this.jsonmenulist.sort();
}
SetMenuList() {
this._UserspecificmenuaccessService.getRMA("driver")
.subscribe((lst) => {
if (lst && lst.length > 0) {
localStorage.setItem('menulist', JSON.stringify(lst));
this.menulist = localStorage.getItem('menulist');
console.log(this.menulist); // gets called after this.jsonmenulist.sort?
return true;
}
}, (error) => {
console.error(error)
});
}
Console:
ERROR TypeError: Cannot read property 'sort' of null
[{"mnurecid":"4","mnuid":"menu1","mnuname":"Bin","mnuurl":"/bin/","mnuposno":"1.0","checked":true}, {"mnurecid":"12","mnuid":"menu9","mnuname":"Menu9","mnuurl":"/menu9","mnuposno":"9.0","checked":false}]

You can use toPromise() method from rxjs library to return promise rather than observable.
So In your service
getRMA(type) {
return this.http.post(environment.baseURL + '/getmenulist', { drive:
type }, this.options).map((response) => response.json()
).toPromise().catch(e => {
console.log(e);
)}
}
and In your component use async and await
async SetMenuList() {
let response = await
this._UserspecificmenuaccessService.getRMA("driver")
console.log(response)
}

Not exactly about async broblem, you just use the this.menulist before it's assigned. Just change the way you run your codes.
ngOnInit() {
this.menulist = localStorage.getItem('menulist');
if (this.menulist) {
this.sortMenuList(); // Sorting the menu if we have menulist already
} else {
this.SetMenuList(); // Else call the service and sort the menu then
}
}
sortMenuList() {
this.jsonmenulist = JSON.parse(this.menulist);
this.jsonmenulist.sort(function (a, b) {
return a.mnuposno > b.mnuposno;
});
this.jsonmenulist = this.jsonmenulist.sort();
}
SetMenuList() {
this._UserspecificmenuaccessService.getRMA("driver")
.subscribe((lst) => {
if (lst && lst.length > 0) {
localStorage.setItem('menulist', JSON.stringify(lst));
this.menulist = localStorage.getItem('menulist');
this.sortMenuList(); // Sorting the menu if we have menulist already
}
}, (error) => {
console.error(error)
});
}
By the way, SetMenuList naming should be setMenuList (Just recommended for naming).

You could write the code that need to be executed inside the subscribe function so that it is executed only after the asynchronous operation is done.

Related

How do I ensure I won't replace a content to an old response?

Good day for all,
I am doing a React course and I'd submited the code to the reviewer. He's returned me few comments and there is one comment I'm not being able to solve.
The comment is the following:
Check if (query === this.state.query) to ensure you are not going to replace the contents to an old response
And part of the code is the one below:
updateQuery = (query) => {
this.setState({
query: query
})
this.updateWantedBooks(query);
}
updateWantedBooks = (query) => {
if (query) {
BooksAPI.search(query).then((wantedBooks) => {
if (wantedBooks.error) {
this.setState({ wantedBooks: [] });
} else {
this.setState({ wantedBooks: wantedBooks });
}
})
} else {
this.setState({ wantedBooks: [] });
}
}
Anyone could help me what do am I suppose to do?
Regards.
Code reviewer is right, you don't really want to replace the response if user has entered the very same query.
You have to store somewhere what for user has searched recently:
this.setState({ wantedBooks: [], query });
In case of success response:
this.setState({ wantedBooks, query });
And then check it in case of further searches:
if (query && query !== this.state.query) {
// continue the search only if query is different that current
Instead of relying on an outer member which is open to abuse by other code, you can employ a factory function to more safely trap a member.
As you have discovered, trapping and testing query == this.state.query can be made to work but is arguably not the best solution available.
With a little thought, you can force each call of updateWantedBooks() automatically to reject the previous promise returned by the same function (if it has not already settled), such that any success callbacks chained to the previous promise don't fire its error path is taken.
This can be achieved with a reusable canceller utility that accepts two callbacks and exploits Promise.race(), as follows:
// reusable cancellation factory utility
function canceller(work, successCallback) {
var cancel;
return async function(...args) {
if (cancel) {
cancel(new Error('cancelled')); // cancel previous
}
return Promise.race([
work(...args),
new Promise((_, reject) => { cancel = reject }) // rejectable promise
]).then(successCallback);
};
};
Here's a demo ...
// reusable cancellation factory utility
function canceller(work, successCallback) {
var cancel;
return async function(...args) {
if (cancel) {
cancel(new Error('cancelled')); // cancel previous
}
return Promise.race([
work(...args),
new Promise((_, reject) => { cancel = reject })
]).then(successCallback);
};
};
// delay utility representing an asynchronous process
function delay(ms, val) {
return new Promise(resolve => {
setTimeout(resolve, ms, val);
});
};
function MySpace() {
// establish a canceller method with two callbacks
this.updateWantedBooks = canceller(
// work callback
async (query) => delay(500, query || { 'error': true }), // a contrived piece of work standing in for BooksAPI.search()
// success callback
(wantedBooks => this.setState(wantedBooks)) // this will execute only if work() wins the race against cancellation
);
this.setState = function(val) {
console.log('setState', val);
return val;
};
};
var mySpace = new MySpace();
mySpace.updateWantedBooks({'value':'XXX'}).then(result1 => { console.log('result', result1) }).catch(error => { console.log(error.message) }); // 'cancelled'
mySpace.updateWantedBooks(null).then(result2 => { console.log('result', result2) }).catch(error => { console.log(error.message) }); // 'cancelled'
mySpace.updateWantedBooks({'value':'ZZZ'}).then(result3 => { console.log('result', result3) }).catch(error => { console.log(error.message) }); // {'value':'ZZZ'} (unless something unexpected happened)
Note that canceller() doesn't attempt to abort the asynchronous process it initiates, rather it stymies the success path of the returned promise in favour of the error path.
I think reviewer's point is that response of Search API is asynchronous and result for "query 1" can arrive after user changed his mind and already requested search "query 2". So when response arrive - we need to check if we really interested in it:
updateQuery = query => {
this.setState({
query: query
wantedBooks: []
})
this.updateWantedBooks(query);
}
updateWantedBooks = query => {
if (query) {
BooksAPI.search(query).then((wantedBooks) => {
// if updateQuery("query1) and updateQuery("query2") called in a row
// then response for query1 can arrive after we requested query2
// => for some period of time we'll show incorrect search results
// so adding check if query still the same can help
if (query !== this.state.query) {
// outdated response
return;
} else if (wantedBooks.error) {
// query is okay, but server error in response
this.setState({
wantedBooks: []
})
} else {
// success response to requested query
this.setState({ wantedBooks });
}
})
}
}
Guys I´ve done some tests with your answers, but I realize that somehow the code was behavioring strangely.
So, I've seen in other part of the reviewer comments, a part which I hadn't had seen before do my answer here, the following comment:
Inside 'then' part of the promise check if(query === this.state.query) to ensure you are not going to replace the contents to an old response.
And this "Inside 'then'" has been beating in my brain.
So, I think I've arrived in a satisfatory code; sure, maybe it isn't the definite solution, that's why I want to show here for you and feel free to comment if I'd have to make some improvement. Here below I put the code:
updateQuery = (query) => {
this.setState({
query: query
})
this.updateWantedBooks(query);
}
updateWantedBooks = (query) => {
if (query) {
BooksAPI.search(query).then((wantedBooks) => {
if (wantedBooks.error) {
this.setState({ wantedBooks: [] });
} else if (query !== this.state.query) {
this.setState( { wantedBooks: [] });
} else {
this.setState({ wantedBooks: wantedBooks });
}
})
} else {
this.setState({ wantedBooks: [] });
}
}
Regards

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

Function returns value before inner async results are finished

Im trying to add a helper method to my service that will call another method in the same service which returns an Observable. The helper method needs to return a Boolean. The problem is the logic required to determine whether the return value is true or false is done inside the subscribe block which is obviously async. So even through the value is true I still always get a false returned. Maybe trying to mix synchronous and asynchronous programming like this is not a good approach.
In my component I am trying to make the following call to the service:
this.isAdmin = this.teamsService.isAdmin(this.teamId, this.user['userId'])
Service:
getAssociations(id, postfix) {
return this.httpService.get(this.base, id, postfix)
.map((response) => {
return this.responseHandler.handleData(response);
})
.catch((error) => {
return this.responseHandler.handleError(error);
}
);
}
isAdmin(teamId, userId) {
let isAdmin = false;
this.getAssociations(teamId, '/teamMembers')
.subscribe(
_data => {
if (_data['teamMembers']) {
for (let i = 0; i < _data['teamMembers'].length; i++) {
if (_data['teamMembers'][i]['id'] === userId) {
isAdmin = true;
break;
}
}
}
}
);
return isAdmin;
}
So the isAdmin method always return false for obvious reasons. I initially was dealing with this logic inside the component by doing the following:
private checkIsAdmin() {
this.teamsService.getAssociations(this.teamId, '/teamMembers')
.subscribe(
_data => {
if (_data['teamMembers'] && _data['teamMembers'].length) {
_data['teamMembers'].map(member => {
if (member['id'] === this.user['userId']) {
this.isAdmin = true;
}
});
}
this.spinner.hide();
this.loading = false;
},
_error => {
this.spinner.hide();
}
);
}
This worked fine until I had to repeat this same logic on other components. So I am trying to come up with a way to abstract that logic into the service itself returning a boolean since that is really all I need from the function.
I have tried a few ideas but none of them worked. Any help would be great. Thanks.
You can also move all control logic up into the Rx stream like so:
isAdmin(teamId, userId) {
return this.getAssociations(teamId, '/teamMembers')
.mergeMap(data => data['teamMembers'] != null ? Rx.Observable.empty() : Rx.Observable.from(data['teamMembers']))
.filter(user => user['id'] === userId)
.mapTo(true) // found admin user
.take(1)
.concat(Rx.Observable.of(false));//default state; no admin user
}
Depending on the usage of getAssociations() you can also change its return type signature from Observable<ResponseTeamWrapperSomething> towards Observable<TeamMember> so you can lose the mergeMap operator in this function.
By moving this logic and replacing your custom control flow with platform supplied function you reduce the cognitive overload of what your code is doing. This also reduces the testing burden on you because you do not have to test framework supplied logic.
I changed the call in the component to:
this.teamsService.isAdmin(this.teamId, this.user['userId'])
.subscribe(response => {
this.isAdmin = response;
}
);
And updated the method inside the service to:
isAdmin(teamId, userId) {
return this.getAssociations(teamId, '/teamMembers')
.map(_data => {
let isAdmin = false;
if (_data['teamMembers']) {
for (let i = 0; i < _data['teamMembers'].length; i++) {
if (_data['teamMembers'][i]['id'] === userId) {
isAdmin = true;
break;
}
}
}
return isAdmin;
});
}

How do I return a list of results with rxjs

I'm using Ionic 3, Firebase, and FirebaseAuth. I'm trying to use the currently authenticated user to retrieve a list of objects for this user. Below is my attempt and I'm getting an error
getContacts Error: TypeError: You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
I'm missing something here as this seems like it should update the onNext with each new piece of data.
My attempt:
getContacts() {
this.contactList.length = 0;
this.data.getAuthenticatedUserContactList().subscribe(
(contact => {
this.contactList.push(contact);
}),
(error => console.error(`getContacts Error: ${error}`))
);
}
getAuthenticatedUserContactList() {
return this.afAuth.getAuthUser()
.flatMap(user => this.database.list(`contact-lists/${user.uid}`).take(1))
.flatMap((contactList) => {
if (contactList && contactList.length > 0) {
contactList.forEach(element => {
console.log(`ForEach: ${element.userId}`); // THIS LOGS THE IDS AS EXPECTED
return this.database.object(`/profiles/${uid}`, { preserveSnapshot: true }).take(1);
});
} else {
return Observable.throw(new Error("No bros here"));
}
});
}
Any help would be greatly appreciated.
Solution as per forkJoin recommendation:
getContacts() {
this.contactList.length = 0;
this.data.getAuthenticatedUserContactList().subscribe(
(contact => {
contact.map(userContact => {
this.contactList.push(<UserProfile>(userContact));
});
}),
(error => console.error(`getContacts Error: ${error}`))
);
}
getAuthenticatedUserContactList() {
return this.afAuth.getAuthUser()
.mergeMap(user => this.database.list(`contact-lists/${user.uid}`).take(1))
.mergeMap((contactList) => {
return Observable.forkJoin(contactList.map(element => this.getProfileWithUid(element.userId).map(userProfile => userProfile.val())))
});
}
getProfileWithUid(uid: string) {
this.profileObject = this.database.object(`/profiles/${uid}`, { preserveSnapshot: true });
return this.profileObject.take(1);
}
In the second flatMap() operator you're not returning anything when this condition is true:
contactList && contactList.length > 0
The return statement is inside forEach callback.
I don't know what's the logic of your app but the callback to flatMap always needs to return an Observable (or something listed here: http://reactivex.io/rxjs/class/es6/MiscJSDoc.js~ObservableInputDoc.html)

How to make sure that the Observable.map only executed on succes?

I just want to make sure that code inside map function should be called only on success but not on failure.
delete(department: Department): Observable<Department[]> {
return this.post('/delete', body).map(response => {
let index: number = this.departments.indexOf(department);
if (index > -1) {
this.departments.splice(index, 1);
}
return this.departments;
});
}
I don't know whether the code inside map executes only on success or not. And also, I have to return an Observable so , I can't apply subscribe function here.
Observable#map operator will be executed on success response only (e.g. status 200). Observable#catch operator is intended to catch failures.
Also, Observable#catch operator will catch javascript errors thrown while mapping success response as well. Example:
fetchDashboardData(): Observable<Dashboard> {
return this._http.get(reqUrl, reqOptions)
.map((response: Response) => new Dashboard(response.json().items[0].dashboard))
.catch((error: any) => {
if (error instanceof Error) {
// js error, e.g. response.json().items was an empty array
console.log(error); // => “Cannot read property 'dashboard' of undefined...
return Observable.throw('Incomplete response data!');
} else {
return Observable.throw('Server error!');
}
});
}
Subscribe block:
this.dashboardService.fetchDashboardData().subscribe(
(dashboard: Dashboard) => {
this.dashboard = dashboard;
console.log('Success fetching dashboard data!', dashboard);
},
(errMssg: string) => {
console.error(errMssg); // => 'Incomplete response data!'
// or 'Server error!'
},
() => {
// finally block!
}
);

Categories

Resources