how to conditionally subscribe to another observable on success of first observable? - javascript

I m tring to use concatmap and complete the observables sequentially when queryParams has id it should only call getbearer tokens or else it should navigate to error with securekey but in reality its going to error page with bearer-token
const result = this.activatedRoute.queryParams.pipe(
concatMap((params) => {
if (params.hasOwnProperty('id')) {
sessionStorage.setItem('secureKey', params.id);
return this.httpServiceService.getBearerTokens();
} else {
this.router.navigate(['error'], {
queryParams: { 'error-key': 'secure-key' },
});
}
})
);
result.subscribe(
(response: any) => {
if (response.access_token) {
sessionStorage.setItem('bearerToken', response.access_token);
this.router.navigate(['summary']);
} else {
this.router.navigate(['error'], {
queryParams: { 'error-key': 'bearer-token' },
});
}
},
(error) => {
this.router.navigate(['error'], {
queryParams: { 'error-key': 'bearer-token' },
});
}
);

I think concatMap is just asking for a race condition (I don't know how often and how easy the query parameters change, though). I would use switchMap instead, because I imagine that the moment we navigate from "?id=alice" to "?id=bob", the matter of Alice's token becomes immaterial.
Generally, conditionally subscribing to another stream should work just fine with switchMap and filter:
firstStream$.pipe(
filter(result => someCondition(result)),
switchMap(result => getAnotherStream(result))
).subscribe(//...
Oh, and by the way, your concatMap returns a stream only if (params.hasOwnProperty('id')). Does that even compile? Return an observable in the else branch as well, it could even be EMPTY, but probably should be throwError.

Related

How do I make sure one Subcription finishes before another?

globleVariable: any;
ngOnInit() {
// This doesn't work. methodTwo throws error saying "cannot read someField from null. "
this.methodOne();
this.methodTwo();
}
methodOne() {
this.firstService.subscribe((res) => { this.globleVariable = res });
}
methodTwo() {
this.secondService.subscribe((res) => { console.log(this.globleVariable.someField) });
}
As shown above, methodOne set the value of globleVariable and methodTwo uses it, therefore the former must finish running before the latter.
I am wondering how to achieve that.
Instead of subscribing in the methods, combine them into one stream and subscribe to that in ngInit(). You can use tap to perform the side effect of updating globaleVariable that you were previously performing in subscribe().
In the example below the "methods" are converted into fields since there is no reason for them to be methods anymore (you can keep them as methods if you want). Then the concat operator is used to create a single stream, where methodOne$ will execute and then when it's complete, methodTwo$ will execute.
Because concat executes in order, you are guaranteed that globaleVariable will be set by methodOne$ before methodTwo$ begins.
globleVariable: any;
methodOne$ = this.someService.pipe(tap((res) => this.globleVariable = res));
methodTwo$ = this.someService.pipe(tap((res) => console.log(this.globleVariable.someField));
ngOnInit() {
concat(this.methodOne$, this.methodTwo$).subscribe();
}
You can create a subject for which observable 2 will wait to subscribe like below :-
globalVariable: any;
subject: Subject = new Subject();
methodOne() {
this.someService.subscribe((res) => { this.globleVariable = res; this.subject.next(); });
}
methodTwo() {
this.subject.pipe(take(1), mergeMap(() => this.someService)).subscribe((res) => {
console.log(this.globleVariable.someField) });
}
The only way to guarantee a method call after a subscription yields is to use the subscription callbacks.
Subscriptions have two main callbacks a success and a failure.
So the way to implement a method call after the subscription yeilds is to chain it like this:
globleVariable: any;
ngOnInit() {
this.methodOne();
}
methodOne() {
this.someService.subscribe((res) => {
this.globleVariable = res
this.methodTwo(); // <-- here, in the callback
});
}
methodTwo() {
this.someService.subscribe((res) => { console.log(this.globleVariable.someField) });
}
You might want to chain the calls with some other rxjs operators for a more standard usage.
ngOnInit() {
this.someService.method1.pipe(
take(1),
tap(res1 => this.globleVariable = res1)
switchmap(res1 => this.someService.method2), // <-- when first service call yelds success
catchError(err => { // <-- failure callback
console.log(err);
return throwError(err)
}),
).subscribe(res2 => { // <-- when second service call yelds success
console.log(this.globleVariable.someField) });
});
}
Please remember to complete any subscriptions when the component is destroyed to avoid the common memory leak.
my take,
so it's a bit confusing when you use same service that throws different results, so instead of someService I used firstService and secondService here.
this.firstService.pipe(
switchMap(globalVariable) =>
this.secondService.pipe(
map(fields => Object.assign({}, globalVariable, { someField: fields }))
)
)
).subscribe(result => {
this.globalVariable = result;
})
What I like about this approach is that you have the flexibility on how you want to use the final result as it is decoupled with any of the property in your class.

Whats the correct method to chain Observable<void> with typescript (Angular)?

I've been searching for the right/best/correct way to chain a few Observable methods. I've read on the using pipe() and map() and so on, but i'm not 100% i fully understand. The basis is i have a few actions to carry out, which some needs to be in sequence, and some dont.
Below are the 4 methods i need to call.
createOrder(order:Order):Observable<void>{
return new Observable((obs)=>
//Do Something
obs.complete();
)
}
updateCurrentStock(order:Order):Observable<void>{
return new Observable((obs)=>
//Do Something
obs.complete();
)
}
updateSalesSummary(order:Order):Observable<void>{
return new Observable((obs)=>
//Do Something
obs.complete();
)
}
updateAnotherDocument(order:Order):Observable<void>{
return new Observable((obs)=>
//Do Something
obs.complete();
)
}
From this 4, the flow should be createOrder ->updateCurrentStock->updateSalesSummary, updateAnotherDocument.
As of now, what i have is
var tasks = pipe(
flatMap(e => this.createOrder(order)),
flatMap(e => this.updateCurrentStock(order)),
flatMap(e => forkJoin([this.updateSalesSummary(order),this.updateAnotherDocument(order)])),
);
of(undefined).pipe(tasks).subscribe({
error: (err) => {
console.log(err);
obs.error(err);
},
complete: () => {
console.log('completed');
obs.complete();
}
});
It works, but i'm not sure if this is the right/cleanest way of doing it and if there is any possible issues in the future.
Using concat
concat will subscribe to your streams in order (not starting the next until the previous one completes.
This should be roughly equivalent.
One difference here is that unlike mergeMap, you're not transforming the output of an api call, it still gets emitted. Since you're not doing anything with the next callback in your subscription, it'll still look similar in the case.
concat(
this.createOrder(order),
this.updateCurrentStock(order),
forkJoin([
this.updateSalesSummary(order),
this.updateAnotherDocument(order)
])
).subscribe({
error: concosle.log,
complete: () => console.log('completed');
});
An Aside:
Here's how I would re-write your original code to be a bit easier to read.
this.createOrder(order).pipe(
mergeMap(_ => this.updateCurrentStock(order)),
mergeMap(_ => forkJoin([
this.updateSalesSummary(order),
this.updateAnotherDocument(order)
]),
).subscribe({
error: (err) => {
console.log(err);
obs.error(err); // <-- What's obs here?
},
complete: () => {
console.log('completed');
obs.complete();
}
});
There are a lot of rxjs operators, I recommend you read https://www.learnrxjs.io/learn-rxjs.
When you have an observable that depends on other's value use switchMap
Example:
const userId$: Observable<number>;
function getUserData$(userId: number): Observable<Data> {
// yourService.fetchUser(userId);
}
userId$.pipe(switchMap((userId: number) => getUserData$(userId)));
When you do not care about the order, you can use:
if you want to emit the last value when all observables complete: forkJoin
if you want to emit every value as any observable emits a value: combineLatest
Example:
const userId$: Observable<number>;
function getUserAge$(userId: number): Observable<number> {
// ...
}
function getUserName$(userId: number): Observable<String> {
// ...
}
userId$.pipe(
switchMap((userId: number) =>
forkJoin([getUserAge$(userId), getUserName$(userId)])
)
);
In your case I think the order does not matter, as none of your observables needs data from the previous one. So I think you should use combineLatest.
If the order of emission and subscription of inner observables is important, try concatMap.

Fetching data from multiple service on single component using RXJS in angular

I have two services with
ser1.
getdata1() {
this.http.get<{message:string,Data1:any}>('http://localhost:3000/api/1')
.pipe(map((data1)=>{
return Data1.Data1.map(data=>{
return {
id: data._id,
data1Title:data1.data1Title,
}
})
})).subscribe((data1) => {
this.data1=data1
this.serv1Subject.next([...this.data1])
})
}
getData1Listener() {
return this.serv1Subject.asObservable()
}
ser2
getdata2() {
this.http.get<{message:string,Data2:any}>('http://localhost:3000/api/2')
.pipe(map((data2)=>{
return Data2.Data2.map(data=>{
return {
id: data._id,
data2Title:data2.data1Title,
}
})
})).subscribe((data2) => {
this.data2=data2
this.serv2Subject.next([...this.data2])
})
}
getData2Listener() {
return this.serv2Subject.asObservable()
}
Now on componentx i need to fetch the data1 and data2 in nginit and after the data is available need to perform an functionY
How can i use subscribe to trigger an functionY?
In componentx.ts
ngOnInit() {
this.Service1OberableSubject = this.serv1.getData1Listener().subscribe((data1) => {
this.data1 = data1;
})
this.Service2OberableSubject = this.serv2.getData2Listener().subscribe((data2) => {
this.data2 = data2;
})
this.serv1.getdata1()
this.serv2.getdata2()
}
Possibly something like this. Use tap to tap into the response and do anything needed.
And forkJoin will merge the response and give it in an array, the first index will the be response of the first observable that was passed in.
getdata1() {
this.http.get<{message:string,Data1:any}>('http://localhost:3000/api/1').pipe(
map((data1)=>{
return Data1.Data1.map(data=>{
return {
id: data._id,
data1Title:data1.data1Title,
}
})
}),
tap((data1)=>{
//save to data1 if needed
this.serv1Subject.next([...this.data1]))
})
)
}
getdata2() {
this.http.get<{message:string,Data2:any}>('http://localhost:3000/api/2').pipe(
map((data2)=>{
return Data2.Data2.map(data=>{
return {
id: data._id,
data2Title:data2.data1Title,
}
})
}),
tap((data2)=>{
//save to data2 if needed
this.serv2Subject.next([...this.data2]))
})
)
}
forkJoin(
getdata1(),
getdata2()
).subscribe((x:any[])=>this.functionY(x));
functionY([a, b]){
console.log({a: a,b: b});
}
You can use forkJoin from rxjs.
import { forkJoin } from 'rxjs';
component.ts
ngOnInit() {
forkJoin([this.serv1.getData1Listener(), this.serv2.getData2Listener()]).subscribe(data => {
this.data1 = data[0];
this.data2 = data[1];
});
}
Here's an example of how you can have if implemented:
import { zip } from 'rxjs';
import { first } from 'rxjs/operators';
ngOnInit() {
this.serv1.getdata1()
this.serv2.getdata2()
zip(this.serv1.getData1Listener(), this.serv2.getData2Listener())
.pipe(first())
.subscribe(([data1, data2]) => {
this.data1 = data1;
this.data2 = data2;
functionY(data1, data2)
})
}
forkJoin should be the best operator in this scenario. It is equally important to understand other higher order operators, this will help to know when to use those.
For e.g.
concatMap — helps to map observables in sequence while waiting for previous observable to complete. The best use case would be a sequential form save.
mergeMap — helps mapping of observables running in parallel. All the previous Observables are kept alive.
switchMap — switchMap is the way to go if we want to cancel previous subscriptions. Type-ahead feature is best implemented using switchMap combined with deboundTime() and distinctUntilChanged().
exhaustMap — used for ignoring newly emitted values till ongoing observable is complete. Best used when we are saving data on click of a button, which has a probability of being clicked multiple times.

Angular RXJS retry a task based on a value

Ok, so I have a service that checks to see if a particular 3rd party JS plugin has loaded. I want to listen in for when it has entered the DOM, meanwhile it is in an undefined state. How do I do that? So far I have tried using a subject and retrying periodically but I can't get it to work:
$apiReady: Subject<boolean> = new Subject();
RegisterOnNewDocumentLoadedOnDocuViewareAPIReady(reControl: any): any {
this.$apiReady.asObservable().subscribe((isReady: boolean) => {
if (isReady) {
//Do something
return of(isReady);
}
})
let IsreControlInitialized = ThirdPartyAPI.IsInitialized(reControl);
if (IsreControlInitialized) {
this.$apiReady.next(true);
}
return throwError(false);
}
Then in the component:
this._apiService.RegisterOnAPIReady(this.elementID).pipe(
retryWhen(error => {
return error.pipe(delay(2000)); //<---- Doesn't work
})).subscribe((response: boolean) => {
if (response) {
//Do some stuff
}
});
My intentions were to check if the API element had been loaded, if not retry in 2 seconds but this doesn't work, can anyone help?
Throwing and catching an error until some condition is met is a little counter-intuitive to me.
My approach consists of using the interval operator along with takeUntil operator.
apiReady = new Subject();
interval(2000) // Check every 2s
.pipe(
map(() => this.isApiReady())
takeUntil(this.apiReady)
)
.subscribe(isReady => {
if (isReady) {
this.apiReady.next();
this.apiReady.complete();
}
})

How to refetch until certain value is returned from the server? (React-Apollo)

With React-Apollo, is it possible to refetch again until the fetched data has a certain value?
Say I have a component who keeps pinging the server until the server gives back a certain response.
graphql(gql`
query {
ping {
response
}
}
`)(MyComponent)
The server either returns
ping: {
response: "busy"
}
or
ping: {
response: "OK"
}
I want this component to keep pinging the server every one second (polling) until the response is "OK". What is the easiest way to do it with Apollo?
Basically all you need to do is set up a query with an option pollInterval and when you get the wanted response call the stopPolling function that arrives on the data in the props function. And make sure the fetchPolicy is set to 'network-only' that is compatible with polling.
Read about options.pollInterval here, about options.fetchPolicy here and about the structure of the data prop here.
This code should work for you:
const PingQuery = gql`
query {
ping {
response
}
}
`
graphql(PingQuery, {
options: {
fetchPolicy: 'network-only', // we don't want to get the response from the cache
pollInterval: 1000 // in milliseconds,
},
props: ({data: {loading, ping, stopPolling}) => {
if (loading) {
return // still loading, ping is undefined
}
if (ping.response === 'busy') {
// still busy
return
}
// not busy.. stop polling and do something
stopPolling()
}
})(MyComponent)
I may not have a perfect answer but I may have something to point you in the right direction.
The graphql() higher order component, as you are probably aware, takes a second parameter of options. You can specify things like a polling interval to continually repeat the query.
In this article, they explain how they were able to dynamically change this polling interval based on specific conditions.
https://dev-blog.apollodata.com/dynamic-graphql-polling-with-react-and-apollo-client-fb36e390d250
The example is using the recompose library, but I imagine you could do something similar like this.
import { graphql } from "react-apollo";
import gql from "graphql-tag";
import { compose, withState, lifecycle } from "recompose";
const QUERY = gql`
query {
ping {
response
}
}
`;
const withPing = compose(
withState("polling", "setPolling", true),
graphql(
QUERY,
{
options: props => {
if (props.polling === true) {
return {
pollInterval: 1000 // Repeat query every 1 second
};
} else {
return { } // or return nothing
}
}
}
),
lifecycle({
componentWillReceiveProps({
data: { loading, ping },
polling,
setPolling
}) {
if (loading) {
return;
}
if (ping.response === 'OK') {
return setPolling(false) // Stops polling
} else if (ping.response === 'busy') {
return setPolling(true) // Continues polling
}
}
})
);
const ComponentWithPing = withPing(Component);
I don't know if this would acctually work, but it should be close.
Another avenue you could checkout is the data.refetch() method on the data response object.
https://www.apollographql.com/docs/react/basics/queries.html#graphql-query-data-refetch.
Best of luck!
You can read more about the options here https://www.apollographql.com/docs/react/basics/queries.html#graphql-query-options and specifically the pollInterval here https://www.apollographql.com/docs/react/basics/queries.html#graphql-config-options-pollInterval
You may want to use Subscriptions.
Example with Hooks:
useSubscription(
gql`
subscription {
ping {
response
}
}
`
)
And of course, the useSubscription Hook accepts a second parameter for options, so you could set your arguments like this:
useSubscription(YOUR_SUBSCRIPTION, { variables: { foo: bar } })

Categories

Resources