Why I can't dispatch multiple action with a map? - rxjs

I am trying to understand something Inside my epic
const loginEpic = (action$, state$) => action$.pipe(
ofType(UsersActions.loginRequest),
switchMap((action: {payload:{email: string, password: string}}) =>
HttpService.PostAsync<apiModels.api_token_auth_request, apiModels.api_token_auth_response>('token-auth', action.payload).pipe(
switchMap(response => {
let token = response && response.data && response.data.token ? response.data.token : '';
return of(
UsersActions.setUserAuth({authKey: token}),
UsersActions.loginSuccess(),
WorkspaceActions.getWorkspacesRequest()
);
}),
catchError((error: string) => {
return of(UsersActions.loginFailed({error}));
})
)
)
);
Why this works
switchMap(response => {
let token = response && response.data && response.data.token ? response.data.token : '';
return of(
UsersActions.setUserAuth({authKey: token}),
UsersActions.loginSuccess(),
WorkspaceActions.getWorkspacesRequest()
);
}),
But this will not work
map(response => {
let token = response && response.data && response.data.token ? response.data.token : '';
return concat(
of(UsersActions.setUserAuth({authKey: token})),
of(UsersActions.loginSuccess()),
of(WorkspaceActions.getWorkspacesRequest())
);
}),
Uncaught Error: Actions must be plain objects. Use custom middleware
for async actions.
Isn't the second one also a first stream ?

switchMap will subscribe to the inner Observable returned (in this case of) and reemit all its items. So of emits 3 items that are propagated further.
On the other hand, map just takes the value returned from its "project" function and propagates it further. map doesn't care what the returned value is. No further logic is applied.
So in your case map is propagating an instance of Observable which is not a valid action.

Related

How to map an Observable with values from another observable

I obtain my Ofertas here
getOfertasByYear(year:number): Observable<Oferta[]> {
return this.http.get<Oferta[]>(`${this.urlWebAPI}/ofertas/year/${year}`)
.pipe(
map(ofertas=>
ofertas.map(oferta=>({
...oferta,
añoPresentada:new Date(oferta.fechaPresentacionFulcrum).getFullYear(),
organismoId:¿¿¿???
}) as Oferta)
),
tap(data => console.log('OfertasService-getOfertasByYear(): ', data)
),
catchError(this.handleError)
)
}
But I need to calculate his organismoId and that is here
getOrganismoDeOferta(ofertaId:string): Observable<Organismo> {
return this.http.get<Organismo>(`${this.urlWebAPI}/organismos/oferta/${ofertaId}`)
.pipe(
tap(//data=>console.log('OfertasService-getOrganismos(): ', data)
),
catchError(this.handleError)
)
}
And I don't know how to pass the result of this Observable to te mapped property
getOfertasByYear(year:number): Observable<Oferta[]> {
return this.http.get<Oferta[]>(`${this.urlWebAPI}/ofertas/year/${year}`)
.pipe(
map(ofertas=>
ofertas.map(oferta=>({
...oferta,
añoPresentada:new Date(oferta.fechaPresentacionFulcrum).getFullYear(),
organismoId:this.getOrganismoDeOferta(oferta.id).subscribe(data=>{
¿¿¿¿??????
})
}) as Oferta)
),
tap(data => console.log('OfertasService-getOfertasByYear(): ', data)
),
catchError(this.handleError)
)
}
I subscribe to it but I don't know how to make the assignment
I have tried to obtain all Ofertas and All Concursos but neither
ofertas$ = this.dataService.getOfertas();
concursos$ = this.dataService.getConcursos();
ofertasConOrganismos$ = forkJoin([
this.ofertas$,
this.concursos$
])
.pipe(
map(([ofertas, concursos]) =>
ofertas.map(oferta => ({
...oferta,
organismoId: concursos.find(c => c.id == oferta.concursoId).organismoId
}) as Oferta))
);
But I get this error:
Cannot read properties of undefined (reading 'organismoId')
Any idea, please?
Thanks
Instead of using a plain map and then calling .subscribe(), you can use a "Higher-Order Mapping Operator" to handle the inner subscription for you. In this case, let's use switchMap.
The idea is to return an observable inside switchMap that emits the data you need. Since you need to make multiple calls, we can leverage some help from forkJoin.
With forkJoin you pass in an array of observables, and it will emit an array of the results. So here below we map the array of Oferta to an array of observables that will each emit the Oferta with the organismoId appended:
getOfertasByYear(year: number): Observable<Oferta[]> {
return this.http.get<Oferta[]>(`${this.urlWebAPI}/ofertas/year/${year}`).pipe(
switchMap(ofertas => forkJoin(
ofertas.map(oferta => this.appendOrganismo(oferta))
)),
catchError(this.handleError)
)
}
Nothing too fancy for the definition of appendOrganismo(); we just make the http call, then map the result to the desired shape:
private appendOrganismo(oferta: Oferta) {
return this.getOrganismoDeOferta(oferta.id).pipe(
map(organismo => ({
...oferta,
añoPresentada: new Date(oferta.fechaPresentacionFulcrum).getFullYear(),
organismoId: organismo.id
}))
);
}

rxjs ForkJoin but have it complete when 1 of 2 observables give a value

I'm new to rsjx but I'm looking for a solution like the Forkjoin but it should complete when 1 of the 2 observables have a value. It needs to be like a ForkJoin because I need to know which observable got a value.
example:
I'm loading categories and I have an autocomplete. Categories is an observable and the valueChanges is returned as an observable.
#Select(IngredientCategoryState.selectIngredientCategories) ingredientCategories$!: Observable<IngredientCategory[]>;
this.filter = this.ingredientForm.controls['category'].valueChanges.pipe(
map((data) => {
if (typeof data === 'object') {
return data.name;
}
return data;
})
)
Then I used the forkJoin but then it requires both observables to have a value
this.filteredIngredientCategories$ = forkJoin({
ingredientCategories: this.ingredientCategories$,
filter: this.filter
}).pipe(
map(({ ingredientCategories, filter }) => {
return this._filter(ingredientCategories, filter);
})
);
UPDATE
I solved it with a combineLatest and triggering the autocomplete field
combineLatest([this.ingredientCategories$, this.filter$])
.pipe(
map((data) => {
return this._filter(data[0], data[1]);
})
).subscribe((ingredientCategories) => {
this.filteredIngredientCategories = ingredientCategories;
});
this.ingredientForm.controls['category'].setValue('');
You can use race to use the first source to emit.
I need to know which observable got a value
Instead of "knowing" which one emitted, you can transform the output of each source to return the object shape you need:
this.filteredIngredientCategories$ = race(
this.ingredientCategories$.pipe(
map(ingredientCategories => ({ ingredientCategories, filter: undefined }))
),
this.filter.pipe(
map(filter => ({ filter, ingredientCategories: undefined }))
)
).pipe(
map(({ ingredientCategories, filter }) => {
return this._filter(ingredientCategories, filter);
})
);
Here's a working StackBlitz demo.

how to use multicasting obs with behavioursubject?

In general we need behavior subject functionality. But only on first subscription we should send subscribe to server in REST. And to send unsubscribe on the last unsubscribe, and all late observers subscribed will gwt the latest json recwived from the first. can i do it using rxjs operaTors and how? or shoul i use custom obserbale ?
currently the custom code for this is this:
public observable: Observable<TPattern> = new Observable((observer: Observer<TPattern>) => {
this._observers.push(observer);
if (this._observers.length === 1) {
this._subscription = this.httpRequestStream$
.pipe(
map((jsonObj: any) => {
this._pattern = jsonObj.Data;
return this._pattern;
})
)
.subscribe(
(data) => this._observers.forEach((obs) => obs.next(data)),
(error) => this._observers.forEach((obs) => obs.error(error)),
() => this._observers.forEach((obs) => obs.complete())
);
}
if (this._pattern !== null) {
observer.next(this._pattern); // send last updated array
}
return () => {
const index: number = this._observers.findIndex((element) => element === observer);
this._observers.splice(index, 1);
if (this._observers.length === 0) {
this._subscription.unsubscribe();
this._pattern = null; // clear pattern when unsubscribed
}
};
});
Sounds like you need a shareReplay(1), it will share the latest response with all subscribes.
const stream$ = httpRequestStream$.pipe(
shareReplay(1),
),
stream$.subscribe(); // sends the request and gets its result
stream$.subscribe(); // doesn't send it but gets cached result
stream$.subscribe(); // doesn't send it but gets cached result
stream$.subscribe(); // doesn't send it but gets cached result

Intercept each request with before and after hooks

I have to make 5 requests (order doesn't matter) to 5 different endpoints. The URL of these endpoints is the same, except for the business line. These business lines are the array of the from.
I want show a skeleton loader before each request and hide once it finish. So, basically the flow is:
1. [Hook - before request]
2. [Log of data fetched]
3. [Hook - after request]
This is my service:
export function getInsurances(
userIdentity: string,
hooks?: RequestHooks
): Observable<Policy[]> {
return from(["all", "vehicle", "health", "soat", "plans"]).pipe(
tap(() => hooks?.beforeRequest && hooks.beforeRequest()),
flatMap<string, Observable<Policy[]>>(businessLine => {
return InsurancesApi.getPolicies<Policy>(
userIdentity,
businessLine
).pipe(
map(policies => {
return policies.map(policy => PolicyStandarizer(policy));
}),
finalize(() => {
hooks?.afterRequest && hooks.afterRequest();
})
);
}),
catchError(err => of(err)),
takeUntil(HttpCancelator)
);
}
This is my subscribe:
const hooks = {
beforeRequest() {
Log.info("Before Request");
setStatus(HttpRequestStatus.PENDING);
},
afterRequest() {
Log.warn("After Request");
setStatus(HttpRequestStatus.RESOLVED);
},
};
getInsurances(userIdentity, hooks).subscribe(
policies => {
Log.normal("Policies:", policies);
setInsurances(policies);
},
(err: Error) => {
setError(err);
}
);
And have this output (sorry for paste the link, I can't embed the image because rep):
https://i.stack.imgur.com/Nbq49.png
The finalize is working fine, but the tap is executing five times at once.
Thank you.
I think you get this behavior because from emits the items synchronously, so its essentially the same as doing:
for (let i = 0; i < arr.length; i++) {
console.log('before req');
observer.next(arr[i]);
}
observer.complete();
afterRequest is shown properly because the actions involved are asynchronous.
If you want to trigger that event only once, before all the requests are fired, you could try this:
from([...])
.pipe(
finalize(() => hooks?.beforeRequest && hooks.beforeRequest()),
flatMap(/* ... */)
)
EDIT - log event before each request
flatMap(
value => concat(
of(null).pipe(
tap(() => hooks?.beforeRequest && hooks.beforeRequest()),
ignoreElements(), // Not interested in this observable's values
),
InsurancesApi.getPolicies(/* ... */)
)
)

RxJS throttle for AJAX requests

I want to create a function that will make AJAX requests to backend. And if this function is called many times at the same time, then it should not make many identical requests to the server. It must make only 1 request.
For example:
doAJAX('http://example-1.com/').subscribe(res => console.log); // must send a request
doAJAX('http://example-1.com/').subscribe(res => console.log); // must NOT send a request
doAJAX('http://example-2.com/').subscribe(res => console.log); // must send a request, bacause of different URL
window.setTimeout(() => {
doAJAX('http://example-2.com/').subscribe(res => console.log); // must send a request because too much time has passed since the last request
}, 3000)
All function calls should return a result, as if the request was actually made.
I think for this purpose I can use RxJS library.
I have done this:
const request$ = new Subject < string > ();
const response$ = request.pipe(
groupBy((url: string) => url),
flatMap(group => group.pipe(auditTime(500))), // make a request no more than once every 500 msec
map((url: string) => [
url,
from(fetch(url))
]),
share()
);
const doAJAX = (url: string): Observable <any> {
return new Observable(observe => {
response$
.pipe(
filter(result => result[0] === url),
first(),
flatMap(result => result[1])
)
.subscribe(
(response: any) => {
observe.next(response);
observe.complete();
},
err => {
observe.error(err);
}
);
request$.next(url);
});
}
I create request$ subject and response$ observable. doAjax function subscribes for response$ and send URL string to request$ subject. Also there are groupBy and auditTime operators in request$ stream. And filter operator in doAJAX function.
This code works but I think it is very difficult. Is there a way to make this task easier? Maybe RxJS scheduler or not use RxJS library at all
As the whole point of this is to memoize Http results and delay repeated calls, you might consider your own memoization. Example:
const memoise = (func) => {
let cache: { [key:string]: Observable<any> } = {};
return (...args): Observable<any> => {
const cacheKey = JSON.stringify(args)
cache[cacheKey] = cache[cacheKey] || func(...args).pipe(share());
return cache[cacheKey].pipe(
tap(() => timer(1000).subscribe(() => delete cache[cacheKey]))
);
}
}
Here is a Stackblitz DEMO

Resources