rxjs Observable: handle unsubscribe of all subscribe - javascript

I have a method that return a Observable
subFoo(id): Observable<number> {
return new Observable<number>(observer => {
setTimeout(() => {
observer.next(id);
}, 1000);
});
}
now i subscribe it three time, and after 5 second unsubscribe all:
const sub1 = subFoo(1).subscribe(result => console.log(result));
const sub2 = subFoo(2).subscribe(result => console.log(result));
const sub3 = subFoo(3).subscribe(result => console.log(result));
setTimeout(() => {
sub1.unsubscribe();
sub2.unsubscribe();
sub3.unsubscribe();
}, 5000);
i can handle the complete unsubscrite of all listners?
eg. (in pseudo code):
subFoo(id): Observable<number> {
return new Observable<number>(observer => {
// something like this
observer.onAllListenerAreUnsubscribed(() => {
console.log('All Listener Are Unsubscribed!');
});
setTimeout(() => {
observer.next(id);
}, 1000);
});
}
Live demo: https://stackblitz.com/edit/angular-ayl12r

EDIT July 2022: The same functionality can be achieved since RxJS 7.0 with tap({ subscribe: () => ... }).
An Observable can't know about subscriptions to its chains. If you want to be able to tell how many times someone subscribed you can count it yourself:
let subscriptions = 0;
subFoo(id): Observable<number> {
return new Observable<number>(observer => {
subscriptions++;
...
return (() => {
if (--subscriptions === 0) {
// do whatever...
}
...
})
})
})
You can also collect all subscriptions on the "observer side" into a single Subscription and then add a custom handler when you unsubscribe:
const subs = new Subscription();
subs.add(subFoo(1).subscribe(...));
subs.add(subFoo(2).subscribe(...));
subs.add(subFoo(3).subscribe(...));
subs.add(() => {
// do whatever...
});
subs.unsubscribe(); // Will unsubscribe all subscriptions and then call your custom method.

you can unsubscribe all listeners in one single line so no need to handle that event
subscriptions.add(sub1).add(sub2).add(sub3);
// Then unsubscribe all of them with a single
subscriptions.unsubscribe();

By completing all your observables at once, you are sure you will not get any data leakage.
You can create a subject that will emit once the observables should stop emitting and use the takeUntil() operator on your observables, like so:
const completeSubscription: Subject<void> = new Subject();
const sub1 = subFoo(1)
.pipe(takeUntil(completeSubscription))
.subscribe(result => console.log(result));
const sub2 = subFoo(2)
.pipe(takeUntil(completeSubscription))
.subscribe(result => console.log(result));
const sub3 = subFoo(3)
.pipe(takeUntil(completeSubscription))
.subscribe(result => console.log(result));
setTimeout(() => {
completeSubscription.next();
completeSubscription.complete();
}, 5000);

Related

Rxjs do something on first emit from multiple subscriptions

Is there a clean way to do something on first emit from multiple subscriptions ?
e.g.:
this.subscription1 = this.service.getData1().subscribe(data => {
this.data1 = data;
console.log('1');
});
this.subscription2 = this.service.getData2().subscribe(data => {
this.data2 = data2;
console.log('2');
});
// Do something after first emit from subscription1 AND subscription2
doSomething();
...
doSomething() {
console.log('Hello world !');
}
Output goal:
1
2
Hello world !
1
2
1
1
2
1
2
2
...
There've multiple times where I also needed such a isFirst operator that'll run some predicate only for the first emission. I've slapped together a quick custom operator that uses a single state variable first to decide if the emission is indeed first and run some predicate using the tap operator.
Since it uses tap internally it does not modify the source emission in any way. It only runs the passed predicate when the emission is indeed first.
Try the following
isFirst() operator
export const isFirst = (predicate: any) => {
let first = true;
return <T>(source: Observable<T>) => {
return source.pipe(
tap({
next: _ => {
if (first) {
predicate();
first = false;
}
}
})
);
};
};
For combining multiple streams that will be triggered when any of the source emits, you could use RxJS combineLatest function.
Example
import { Component } from "#angular/core";
import { timer, Observable, Subject, combineLatest } from "rxjs";
import { tap, takeUntil } from "rxjs/operators";
#Component({
selector: "my-app",
template: `<button (mouseup)="stop$.next()">Stop</button>`
})
export class AppComponent {
stop$ = new Subject<any>();
constructor() {
combineLatest(timer(2000, 1000), timer(3000, 500))
.pipe(
isFirst(_ => {
console.log("first");
}),
takeUntil(this.stop$)
)
.subscribe({
next: r => console.log("inside subscription:", r)
});
}
}
Working example: Stackblitz
In your case it might look something like
this.subscription = combineLatest(
this.service.getData1().pipe(
tap({
next: data => {
this.data1 = data;
console.log('1');
}
})
),
this.service.getData2().pipe(
tap({
next: data => {
this.data2 = data;
console.log('2');
}
})
)
).pipe(
isFirst(_ => {
console.log("first");
})
).subscribe({
next: r => console.log("inside subscription:", r)
});
The easiest strategy is to have a 3rd Observable that will perform this action.
See below example
const Observable1$ = timer(1000, 2000).pipe(
map(() => 1),
tap(console.log)
);
const Observable2$ = timer(1700, 1700).pipe(
map(() => 2),
tap(console.log)
);
const Observable3$ = combineLatest([Observable1$, Observable2$]).pipe(
take(1),
map(() => "Hello World!"),
tap(console.log)
);
Observable1$.subscribe();
Observable2$.subscribe();
Observable3$.subscribe();
The console output is as per below, since there are two subscribers to Observable1$ (i.e Observable1$ and Observable3$same as two subscribers toObservable2$(i.eObservable2$ and Observable3$ we see console logs 1 1 2 2 'hello world ...'
Here is the link to the stackblitz
In the above we notice that we get 2 subscriptions hence 2 console logs for each. To solve this we can use Subjects to generate new Observables and combine these instead
const track1Subject$ = new Subject();
const track1$ = track1Subject$.asObservable();
const track2Subject$ = new Subject();
const track2$ = track2Subject$.asObservable();
const Observable1$ = timer(1000, 2000).pipe(
map(() => 1),
tap(console.log),
tap(() => track1Subject$.next()),
take(5)
);
const Observable2$ = timer(1700, 1700).pipe(
map(() => 2),
tap(console.log),
tap(() => track2Subject$.next()),
take(5)
);
const Observable3$ = combineLatest([track1$, track2$]).pipe(
take(1),
map(() => "Hello World!"),
tap(console.log)
);
Observable1$.subscribe();
Observable2$.subscribe();
Observable3$.subscribe();
See Link to final solution
With some further restrictions, this problem becomes easier. Unfortunately, operators like combineLatest, and zip add extra structure to your data. I'll provide a solution with zip below, but it doesn't extend at all (if you want to add more logic downstream of your zip, you're out of luck in many cases).
General solution.
Assuming, however, that getData1 and getData2 are completely orthogonal (How they emit and how they are consumed by your app are not related in any predictable way), then a solution to this will require multiple subscriptions or a custom operator tasked with keeping track of emissions.
It's almost certainly the case that you can do something more elegant than this, but this is the most general solution I could think of that meets your very general criteria.
Here, I merge the service calls, tag each call, and pass through emissions until each call has emitted at least once.
merge(
this.service.getData1().pipe(
tap(_ => console.log('1')),
map(payload => ({fromData: 1, payload}))
),
this.service.getData2().pipe(
tap(_ => console.log('2')),
map(payload => ({fromData: 2, payload}))
)
).pipe(
// Custom Operator
s => defer(() => {
let fromData1 = false;
let fromData2 = false;
let done = false;
return s.pipe(
tap(({fromData}) => {
if(done) return;
if(fromData === 1) fromData1 = true;
if(fromData === 2) fromData2 = true;
if(fromData1 && fromData2){
done = true;
doSomething();
}
})
);
})
).subscribe(({fromData, payload}) => {
if(fromData === 1) this.data1 = payload;
if(fromData === 2) this.data2 = payload;
});
In the subscription, we have to separate out the two calls again. Since you're setting a global variable, you could throw that logic as a side effect in the tap operator for each call. This should have similar results.
merge(
this.service.getData1().pipe(
tap(datum => {
console.log('1')
this.data1 = datum;
),
map(payload => ({fromData: 1, payload}))
),
...
The zip Solution
This solution is much shorter to write but does come with some drawbacks.
zip(
this.service.getData1().pipe(
tap(datum => {
console.log('1')
this.data1 = datum;
)
),
this.service.getData2().pipe(
tap(datum => {
console.log('2')
this.data2 = datum;
)
)
).pipe(
map((payload, index) => {
if(index === 0) doSomething();
return payload;
})
).subscribe();
What is passed into your subscription is the service calls paired off. Here, you absolutely must set a global variable as a side effect of the original service call. The option of doing so in the subscription is lost (unless you want them set as pairs).

how to fix the unsubscribe in angular

TS
tempThermometer = new BehaviorSubject<any>([]);
subscription: Subscription;
const promises = list.map(
(url: any) =>
new Promise(resolve => {
this.subscription = this.global.getData(url.link).pipe(take(1)).subscribe((res) => {
const urlArr = new Array();
urlArr.push(url);
this.tempThermometer.value.filter((data: any) => {
if (data.spinning) {
return data.spinning = urlArr.findIndex((x: any) => x.sensor === data.sensor) === -1
}
return;
});
resolve(res);
}, (err: Error) => {
return reject(err);
});
})
);
merge(...observables).subscribe((results) => {
console.log(results);
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
What I want to do here is to unsubscribe the promises, because when I click to other page it still running/fetching a data and I want it to stop when I click to other page.
the unsubscribe doesn't work. how to fix it?
The most basic way is to store the Subscription returned from a call to subscribe, and then calling the unsubscribe method on the Subscription when you leave the page (ngOnDestroy life cycle hook in Angular, more about the lifecycle hooks: here).
In your component:
ngOnInit() {
this.sub = this.something.subscribe( ... )
}
ngOnDestroy() {
this.sub.unsubscribe();
}
There are many other ways too:
Using the async pipe in your template where you need the values. It will unsubscribe automatically for you!
take operator that you used in your example will unsubscribe after N values.
takeWhile operator that will unsubscribe based on a predicate.
Here's an article discussing 6 different ways of unsubscribing: https://blog.bitsrc.io/6-ways-to-unsubscribe-from-observables-in-angular-ab912819a78f

Is there a way to use a observable returning function for each element of another observable array?

I get an Observable<Group[]> from my Firebase collection.
In this Group class is an id which I wanna use to retrieve another dataset array from Firebase, which would be messages for each unique group Observable<Message[]>.(each group has its own chat: Message[])
And it want to return an observable which hold an array of a new Type:
return { ...group, messages: Message[] } as GroupWithMessages
the final goal should be Observable<GroupWithMessages[]>
getGroupWithChat(): Observable<GroupWithMessages[]> {
const groupColl = this.getGroups(); // Observable<Group[]>
const messages = groupColl.pipe(
map(groups => {
return groups.map(meet => {
const messages = this.getMessagesFor(group.uid);
return { messages:messages, ...group} as GroupWithMessages
});
})
);
return messages;
}
}
and here the Message function
getMessagesFor(id: string): Observable<Message[]> {
return this.afs.collection<Message>(`meets/${id} /messages`).valueChanges();
}
sadly that doesnt work because when i create the new Obj I cannot bind messages:messages because messages ist vom typ Observable<Message[]>
I hope that cleares things
UPDATE:
my main problem now comes down to this:
getGroupsWithMessages() {
this.getJoinedGroups()
.pipe(
mergeMap(groups =>
from(groups).pipe(
mergeMap(group => {
return this.getMessagesFor(group.uid).pipe(
map(messages => {
return { ...group, messages } as GroupIdMess;
})
);
}),
tap(x => console.log('reaching here: ', x)),
toArray(),
tap(x => console.log('not reaching here = completed: ', x))
)
),
tap(x => console.log('not reaching here: ', x))
)
.subscribe(x => console.log('not reaching here: ', x));
}
when i call that function my console.log is as follows:
Not sure if I follow what you're doing here but the logic look like you'd want:
getGroupWithChat() {
return this.getGroups.pipe(map(groups=> {
return groups.map(group => this.getMessagesFor(group.uid));
})).subscribe(); // trigger "hot" observable
}
Let me know if I can help further after you clarify.
UPDATE:
So it looks like you need to get the UID of the group before making the call to get the GroupMessages[]?
get Group: Observable
call getMessagesFor(Group.uid)
this example gets groups result$ then
concatMap uses groups result$ to make the messages query
this.getGroups().pipe(
concatMap((group: Group) => this.getMessagesFor(group.uid))
).subscribe((messages: GroupWithMessages[]) => {
console.log(messages);
});
You may still want to map them together but it seems like you know how to do that. concatMap waits for the first to finish, then makes the second call which you need.
Is this closer?
Use forkJoin to wait for messages to be received for all groups. Then map the result of forkJoin to an array of GroupWithMessages like this -
getGroupWithChat(): Observable<GroupWithMessages[]> {
return this.getGroups()
.pipe(
switchMap(groups => {
const messagesForAllGroups$ = groups.map(group => this.getMessagesFor(group.uid));
return forkJoin(messagesForAllGroups$)
.pipe(
map(joined => {
//joined has response like -
//[messagesArrayForGroup0, messagesArrayForGroup1, messagesArrayForGroup2....];
const messagesByGroup = Array<GroupWithMessages>();
groups.forEach((group, index) => {
//assuming that GroupWithMessages has group and messages properties.
const gm = new GroupWithMessages();
gm.group = group;
gm.messages = joined[index];
messagesByGroup.push(gm);
});
return messagesByGroup;
})
)
})
)
}
I usually do that by splitting Observable<any[]> to Observable<any> and then mergeMap the results to inner Observable.
Something like this should work:
getMessagesFor(id: string): Observable<number> {
return of(1);
}
getGroups(): Observable<string[]> {
return of(["1", "2"]);
}
getGroupWithChat() {
this.getGroups().pipe(
mergeMap(groups => from(groups)), // Split the stream into individual group elements instead of an array
mergeMap(group => {
return this.getMessagesFor(group).pipe(
map(messages => {
return Object.assign(group, messages);
})
);
})
);
}
Edit:
Consider BehaviorSubject. It doesn't complete at all:
const behSub: BehaviorSubject<number[]> = new BehaviorSubject([1, 2, 3]);
setTimeout(() => {
behSub.next([4, 5, 6]);
}, 5000);
behSub
.pipe(
mergeMap(arr =>
from(arr).pipe(
tap(), // Do something with individual items, like mergeMap to messages
toArray() // Go back to array
)
)
)
.subscribe(console.log, null, () => {
console.log('Complete');
});

Is it possible to stop a Rx.Subject from emitting within a subscriber like event.stopPropagation?

Using RxJS 5, is this possible?
var source = new Rx.Subject();
source.map((data) => {console.log('map1'); return data;})
.subscribe((data) => {
console.log('subscribe1', data);
if(someCondition) {
source.stop(); //????????
}
});
source.map((data) => {console.log('map2'); return data;})
.subscribe((data) => {
console.log('subscribe2', data);
});
So, when I call source.next("Hello World"); only the first subscriber will be notified. Of course this will fail in source.stop() because the stop function does not exist but it is just to elaborate my question.
Exist a way to do this, like event.stopPropagation do?
It depends on what do you expect by stopping propagation. You can call source.complete() which will stop the Subject and it will never emit anything any more.
See demo: https://jsbin.com/geyucuc/3/edit?js,console
However, if you want to be able to work on "per item basis" you can't the structure you have right now because each value from source is emitted to both its subscribers by the Subject.
You don't have chain where the value goes like source => subscribe1 => subscribe2.
Right now you have source => subscribe1 and then source => subscribe2.
So you can make it a chain like this for example:
var source = new Rx.Subject();
source
.map(item => { // wrap this
return { value: item, stopped: false };
})
// your logic
.map((data) => {
console.log('map1', data.value);
// do whatever here
if (data.value == 2) {
data.stopped = true;
}
return data;
})
.filter(item => !item.stopped) // this is your stopPropagation()
.map((data) => {
// do whatever here
console.log('map2', data.value);
return data;
})
.subscribe((data) => {
// do nothing here, just construct the chain.
});
source.next(1);
source.next(2);
Which prints the following:
map1 1
map2 1
map1 2

Action on subscribe() and unsubscribe()

in RxJS I'd like to take some special action once an Observer subscribes to an Observable and when he unsubscribes. I can of course overwrite the subscribe() and unsubscribe() methods, but that seems crud.
Isn't there a way during creation of the observable to supply callbacks that get called whenever someone subscribes/unsubscribes?
BR,
Daniel
This is what Observable.create is for. You can create your own observable with specified attach/detach handlers, and can even wrap existing observables with just 2 additional lines of code.
const obs = Rx.Observable.create(observer => {
console.log('attach');
// If you want to wrap another observable, call this:
// const subs = other.subscribe(observer);
return () => {
// subs.unsubscribe();
console.log('detach');
};
});
console.log('subscribe subscription1');
const subscribtion1 = obs.subscribe(() => {});
console.log('subscribe subscription2');
const subscribtion2 = obs.subscribe(() => {});
setTimeout(() => {
console.log('subscribtion1.dispose()');
subscribtion1.unsubscribe();
}, 500);
setTimeout(() => {
console.log('subscribtion2.dispose()');
subscribtion2.unsubscribe();
}, 1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.0-rc.4/Rx.js"></script>

Categories