Observables inside of tap() complete - javascript

If I use the tap rxjs operator on an observable to call another observable, can I guarantee that it completes before the rest of the pipe?
The idea here is to have a service make an http call to the backend, if it's a good login, create a cookie, then return a mapped response to the consuming component. I want to make sure the cookie is added before continuing to make sure there are no race conditions.
import { of, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
const httpObservable = loginAccount('fsdfds', 'fdsfsd');
httpObservable.subscribe(x => {
console.log(x);
});
function loginAccount(username, password): Observable<any> {
const httpResponse = of({ loggedIn: false, data: 'faketokenfrombackend' });
return httpResponse.pipe(
// Will this AWLAYS complete before map?
tap(resp => fakeLocalStorage('Do something with the result')),
// Will this AWLAYS complete before map?
tap(resp => fakeLocalStorage('Do something else with the result')),
map(resp => {
if (!resp.loggedIn)
return { success: false, message: 'really bad thing happened' };
else
return {success: true, message: 'WEEEEEE, it worked!'}
}));
}
function fakeLocalStorage(data: string): Observable<boolean> {
console.log('adding token to cookie');
return of(true);
}
The above script outputs this to the console window as expected, but can I rely on it?
adding token to cookie
adding token to cookie
{success: false, message: "really bad thing happened"}

Yes, RxJS will run the piped operators in order. As long as the tap operators are synchronous they will complete before the map operator is run. If they do anything asynchronous they will not.

Related

How do I ensure operators on an observable occur after a HTTP Interceptor?

In my Angular 8 application, I have a basic caching interceptor:
export class CacheInterceptor implements HttpInterceptor {
constructor(private cache: CacheService) {}
public intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
if (req.method !== 'GET') {
return next.handle(req);
}
const cachedResponse = this.cache.get(req);
if (cachedResponse) {
console.log(cachedResponse);
return of(cachedResponse);
}
return next.handle(req).pipe(
filter(event => event instanceof HttpResponse),
map((response: HttpResponse<any>) => {
this.cache.addToCache(req, response);
return response;
})
);
}
}
I also have a service which retrieves data from an external API:
public getCases(options: ModuleArguments): Observable<CaseResponse> {
return this.http
.get<CaseResponse>(this.URL_BASE, {
params: options as HttpParams
})
.pipe(map(this.cleanData, this));
}
The 'cleanData' method just loops through the received data and amends some of the values to make them more human friendly (e.g. turns 'support_request' to 'Support Request').
What appears to be happening is the response is being added to the cache by the CacheInterceptor after the data has been 'cleaned' within the service. Therefore, when the same request is made again, and received from the cache, the service is attempting to clean data which has already been cleaned.
How do I ensure that the the HTTP Response has been intercepted and added to the cache before it has been amended by the service?
How about you approach this by moving the pipe(map(this.cleanData, this)) operation to the point when the Observable has completed and returned your CaseResponse. Likely, this will mean that the HttpInterceptor has been applied first.
i.e. In the place where you invoke getCases you could try something like this:
service.getCases(options).subscribe(resolvedData => {
// assuming cleanData(data: CaseResponse) signature
const cleanedData = this.cleanData(resolvedData);
// .. do something with cleanedData
});
Also, from a design perspective, you wouldn't want getCases to do more than exactly what it's supposed to - It's a service method that performs an HTTP request and returns the cases in the format they are sent to you. The reformatting of the data could be ideally done at the consumer of that service function - as it's very likely the consumer that needs it cleaned/reshaped.

reduce redux-thunk boilersplate code

I'm using redux-thunk middleware, it handles but async action well but I have too many boilerplate to write, how can I reduce it?
export function userReducer(state = {
signup_loading: false,
signup_failed: '',
signup_error: '',
login_loading: false,
login_failed: '',
login_error: ''
}, { payload, payload: { data, error } }) {
switch (action.type) {
case SIGNUP_REQUEST:
return {
signup_loading: true
}
case SIGNUP_SUCCESS:
return {
signup_loading: false,
data
}
case SIGNUP_FAILED:
return {
signup_loading: false,
signup_error: error
}
case LOGIN_REQUEST:
return {
login_loading: true
}
case LOGIN_SUCESS:
return {
login_loading: false,
data
}
case LOGIN_FAILED:
return {
login_loading: false,
login_error: error
}
default: return state
}
}
There are similar things that I want to do for each http call in the entire app:
before request I want to show loader for every http call
hide the loader after it’s finished
if error stop the loader and show the error msg
no way I want to copy paste above code across all the reducer
You can keep seperate actions like, SHOW_LOADER, HIDE_LOADER for handling the loader. These actions can be dispatched when you make a web api call, or when an api call return. These actions can be dispatched from anywhere in your application to handle the loader states. You will have to write that only in one reducer which can be called from anywhere.

AsyncStorage behaviour

I bumped into a weird behaviour of AsyncStorage that I couldn't wrap my head around and would appreciate anyone who can explain to me what's happening behind the scene (i.e. fail cases and why)
Here's a code I'm working on:
componentDidMount() {
let _this = this;
AsyncStorage.getItem('token', (err, data) => {
setTimeout(() => {
if(data !== null){
this.setState({isReady: true, isLoggedIn: true});
store.dispatch({type: t.LOGGED_IN, token: data});
}
else{
this.setState({isReady: true, isLoggedIn: false})
store.dispatch({type: t.LOGGED_OUT});
}
}, 3000)
console.log(err);
});
}
As you can see, I'm passing a callback function to getItem() as per the documentation, which basically tells me if the user has logged in before and hasn't logged out since(i.e. the token still persists in the device/app somewhere).
This code succeeded the first time, retrieving the old token I stored via reducer:
export default function authReducer(state = initialState, action)
{
switch (action.type) {
case t.LOGGED_IN:{
AsyncStorage.setItem('token', action.token);
return Object.assign({}, state, { isLoggedIn: true, token: action.token });
}
case t.LOGGED_OUT:{
AsyncStorage.removeItem('token');
return Object.assign({}, state, {isLoggedIn: false, token: null});
}
default:
return state;
}
}
However, on the second time I reloaded the app, the AsyncStorage will always fail to retrieve the data even after I tried logging in again and again.
I tried variations of AsyncStorage calls as well, i.e. using await, .then plus .catch, but they all lead to the same result.
My questions are:
On fail cases, I was under the impression that getItem() will still call the callback function I passed since there's an error on the param list. However, my console.log was never run in the above case. Am I expecting something I shouldn't be here?
Why would it only keep failing from second time around? Is there a case where calling setItem() on the same key more than once without ever deleting it will cause the storage to fail? (I know for sure that the first try was a success because I printed the retrieved token from async storage)
Does this have anything to do with the fact that I'm loading my app from Expo and initialized the app with CRNA? will this somehow make the asyncStorage persistence quality different?
thanks in advance! :)
EDIT: Upon further inspection, it seems stopping the packager and running it again seems to allow the app to once again succeed in retrieving the old token, but if I refresh the app again, after editing the code, getItem() will fail again. Is this a thing with Expo and persistent storage then?
Reducers must be free from side-effects, since it is a pure function.
As mentioned in the docs
They must be pure functions—functions that return the exact same output for given inputs. They should also be free of side-effects.
Side Effects - when a procedure changes a variable from outside its scope
A better approach would be to use redux-saga, that is like a separate thread for your side effects model.
So the problem is fixed now, thanks to a more experienced programmer friend of mine.
Turns out my mistake is that I put the AsyncStorage.setItem() call in reducer, which he said is deterministic in nature.
I moved the call to the actions.js of that class right before dispatch, and it works!
So instead of
export default function authReducer(state = initialState, action)
{
switch (action.type) {
case t.LOGGED_IN:{
AsyncStorage.setItem('token', action.token);
return Object.assign({}, state, { isLoggedIn: true, token: action.token });
}
case t.LOGGED_OUT:{
AsyncStorage.removeItem('token');
return Object.assign({}, state, {isLoggedIn: false, token: null});
}
default:
return state;
}
}
I did
export default function authReducer(state = initialState, action)
{
switch (action.type) {
case t.LOGGED_IN:{
return Object.assign({}, state, { isLoggedIn: true, token: action.token });
}
case t.LOGGED_OUT:{
return Object.assign({}, state, {isLoggedIn: false, token: null});
}
default:
return state;
}
}
Plus this
export function login(data, successCB, errorCB) {
return (dispatch) => {
api.login(data, function (success, data, error) {
if (success && data.exists) {
AsyncStorage.setItem('token', data.token); //NOTE THIS
dispatch({type: t.LOGGED_IN, token: data.token});
successCB(data);
}else if (error) errorCB(error)
});
};
}
export function signOut(successCB, errorCB){
return (dispatch) => {
AsyncStorage.removeItem('token'); //NOTE THIS
dispatch({type: t.LOGGED_OUT});
successCB();
}
}
But my question still persists (pardon the pun), "Why does that simple modification works? did I understand the mechanism of reducers and dispatchers wrong here?"
Also, what does being deterministic means and have to do with incompatibilities with Async calls?
If anyone can explain the concept to me that would be awesome! :D

Angular 2 subject is not a function

Trying to communicate with 2 components
I thought I'd be able to make a http call, then maybe mergeMap or switchMap to a subject?
Something like
import {Subject} from 'rxjs/Subject';
constructor(private _http: HttpClient) {
this.populateList = new Subject<Blog[]>();
}
getBlogs(){
return this._http.get(this.blogsURL+'blogs')
.map((result: Response ) => {
this.blogs = result['blogs'];
return this.blogs;
}).switchMap((blogs)=>this.populateList.next(blogs))
}
But I get:
You provided 'undefined' where a stream was expected. You can provide
an Observable, Promise, Array
I'm getting errors just trying to subscribe to the subject:
this.blogsService.populateList()
.subscribe((res)=>{
console.log(res)
})
this.blogsService.populateList is not a function
What I'm looking for is a way to update views after http calls
You need to subscribe like this without (). Cause its not a function. surprise
this.blogsService.populateList.subscribe()
and rewrite first function like this cause you dont need switch map you just need to do is side effect to populate list.
getBlogs(){
return this._http.get(this.blogsURL+'blogs')
.map((result: Response ) => {
this.blogs = result['blogs'];
return this.blogs;
}).do((blogs)=>this.populateList.next(blogs))
}

Returning a record with a string in ember

I am trying to implement a search function where a user can return other users by passing a username through a component. I followed the ember guides and have the following code to do so in my routes file:
import Ember from 'ember';
export default Ember.Route.extend({
flashMessages: Ember.inject.service(),
actions: {
searchAccount (params) {
// let accounts = this.get('store').peekAll('account');
// let account = accounts.filterBy('user_name', params.userName);
// console.log(account);
this.get('store').peekAll('account')
.then((accounts) => {
return accounts.filterBy('user_name', params.userName);
})
.then((account) => {
console.log(account);
this.get('flashMessages')
.success('account retrieved');
})
.catch(() => {
this.get('flashMessages')
.danger('There was a problem. Please try again.');
});
}
}
});
This code, however, throws me the following error:
"You cannot pass '[object Object]' as id to the store's find method"
I think that this implementation of the .find method is no longer valid, and I need to go about returning the object in a different manner. How would I go about doing this?
You can't do .then for filterBy.
You can't do .then for peekAll. because both will not return the Promise.
Calling asynchronous code and inside the searchAccount and returning the result doesn't make much sense here. since searchAccount will return quickly before completion of async code.
this.get('store').findAll('account',{reload:true}).then((accounts) =>{
if(accounts.findBy('user_name', params.userName)){
// show exists message
} else {
//show does not exist message
}
});
the above code will contact the server, and get all the result and then do findBy for the filtering. so filtering is done in client side. instead of this you can do query,
this.store.query('account', { filter: { user_name: params.userName } }).then(accounts =>{
//you can check with length accounts.length>0
//or you accounts.get('firstObject').get('user_name') === params.userName
//show success message appropriately.
});
DS.Store#find is not a valid method in modern versions of Ember Data. If the users are already in the store, you can peek and filter them:
this.store.peekAll('account').filterBy('user_name', params.userName);
Otherwise, you'll need to use the same approach you used in your earlier question, and query them (assuming your backend supports filtering):
this.store.query('account', { filter: { user_name: params.userName } });

Categories