Invoke method when no observers for RxJs Subject - rxjs

How to invoke a method when all the observers have unsubscribed from a subject.
Update
const alphaStore = new BehaviourSubject(0);
observer1 = alphaStore.subscribe(console.log);
observer2 = alphaStore.subscribe(console.log);
And when all of these observers unsubscribe. I want a method to be invoked. Like...
Observer1 unsubscribed
Observer2 unsubscribed
All observers left

What you describe already does the finalize() operator. Better said finalize() calls its callback when the chain disposes which means it's called when all observers unsubscribes, the chain completes or errors.
const subject = new Subject();
const shared = subject.pipe(
finalize(() => console.log('finalize')),
share(),
);
https://stackblitz.com/edit/rxjs-rebfba
When all observers unsubscribe share() unsubscribes from its source which triggers finalize().
Currently there's no way to distinguish why finalize() was invoked. See this issue https://github.com/ReactiveX/rxjs/issues/2823 and examples there on how to do it.

You can create a custom Observable, that will track the subscription count.
Heres a simple example:
let count = 0;
const tracked$ = new Observable(() => {
count++;
return ()=>{
count--;
if (count === 0) {
console.log('I am empty');
}
};
})
And then merge it with Observable that does actual work.
For simplicity sake, lets imagine its just a timer
// const tracked$ = ...
const data$ = Observable.timer(0, 5);
const result$ = data$
.merge(tracked$)
.take(5)
.subscribe(value => console.log('v:', value));
After 5 values were emitted -- it will log I am empty.
Heres a live example (with some rewrite and two subscriptions):
https://observable-playground.github.io/gist/4a7415f3528aa125fb686204041138cb
NOTE: this code uses rxjs-compat notation, which is easier to read. Above linked example uses .pipe notation, which is more common now.
Hope this helps.

Related

Subject-like RxJS Observable that transparently pipes through flatMap

When using Dependency injection in Angular I often need to subscribe to an observable that I haven't yet created!
I often end up using something like this:
// create behavior subject OF Observable<number>
const subject = new BehaviorSubject<Observable<number>>(EMPTY);
// subscribe to it, using flatMap such as to 'unwrap' the observable stream
const unwrappedSubject = subject.pipe(flatMap((x: number) => x));
unwrappedSubject.subscribe(s => console.log(s));
// now actually create the observable stream
const tim = timer(1000, 1000);
// set it into the subject
subject.next(tim);
This uses flatMap to 'unwrap' the observable contained in the subject.
This works fine, but frankly it always feels 'icky'.
What I really want is something like this, where the consumer of the subject treats the instance of the Subject as Observable<number> without having to pipe it every usage.
const subject = new UnwrappingBehaviorSubject<number>(EMPTY);
subject.subscribe((x: number) => console.log(x));
// this could use 'next', but that doesn't feel quite right
subject.setSource(timer(1000, 1000));
I'm aware that I could subscribe to the timer and hook it up directly to the subject, but I also want to avoid an explicit subscribe call because that complicates the responsibility of unsubscribing.
timer(1000, 1000).subscribe(subject);
Is there a nice way to achieve this?
The Subject.ts and BehaviorSubject.ts source files get more complicated than I expected. I'm scared I'll end up with horrible memory leaks if I try to fork it.
I think this would be another way to solve it:
foo.component.ts
export class FooComponent {
private futureObservable$ = new Observable(subscriber => {
// 'Saving' the subscriber for when the observable is ready.
this.futureObservableSubscriber = subscriber;
// The returned function will be invoked when the below mentioned subject instance
// won't have any subscribers(after it had at least one).
return () => this.futureObservableSubscription.unsubscribe();
}).pipe(
// You can mimic the Subject behavior from your initial solution with the
// help of the `share` operator. What it essentially does it to *place*
// a Subject instance here and if multiple subscriptions occur, this Subject instance
// will keep track of all of them.
// Also, when the first subscriber is registered, the observable source(the Observable constructor's callback)
// will be invoked.
share()
);
private futureObservableSubscriber = null;
// We're using a subscription so that it's easier to collect subscriptions to this observable.
// It's also easier to unsubscribe from all of them at once.
private futureObservableSubscription = new Subscription();
constructor (/* ... */) {};
ngOnInit () {
// If you're using `share`, you're safe to have multiple subscribers.
// Otherwise, the Observable's callback(i.e `subscriber => {...}`) will be called multiple times.
futureObservable$.subscribe(/* ... */);
futureObservable$.subscribe(/* ... */);
}
whenObservableReady () {
const tim = timer(1000, 1000);
// Here we're adding the subscription so that is unsubscribed when the main observable
// is unsubscribed. This part can be found in the returned function from the Observable's callback.
this.futureObservableSubscription.add(tim.subscribe(this.futureObservableSubscriber));
}
};
Indeed, a possible downside is that you'll have to explicitly subscribe, e.g in the whenObservableReady method.
With this approach you can also have different sources:
whenAnotherObservableReady () {
// If you omit this, it should mean that you will have multiple sources at the same time.
this.cleanUpCrtSubscription();
const tim2 = timer(5000, 5000);
this.futureObservableSubscription.add(tim2.subscribe(this.futureObservableSubscriber));
}
private cleanUpCrtSubscription () {
// Removing the subscription created from the current observable(`tim`).
this.futureObservableSubscription.unsubscribe();
this.futureObservableSubscription = new Subscription();
}

rxjs: why the stream emit twice when another stream use take(1)

When I use take(1), it will console.log twice 1, like below code:
const a$ = new BehaviorSubject(1).pipe(publishReplay(1), refCount());
a$.pipe(take(1)).subscribe();
a$.subscribe((v) => console.log(v)); // emit twice (1 1)
But when I remove take(1) or remove publishReplay(1), refCount(), it follow my expected (only one 1 console.log).
const a$ = new BehaviorSubject(1).pipe(publishReplay(1), refCount());
a$.subscribe();
a$.subscribe((v) => console.log(v)); // emit 1
// or
const a$ = new BehaviorSubject(1);
a$.pipe(take(1)).subscribe();
a$.subscribe((v) => console.log(v)); // emit 1
Why?
Version: rxjs 6.5.2
Let's first have a look at how publishReplay is defined:
const subject = new ReplaySubject<T>(bufferSize, windowTime, scheduler);
return (source: Observable<T>) => multicast(() => subject, selector!)(source) as ConnectableObservable<R>;
multicast() will return a ConnectableObservable, which is an observable that exposes the connect method. Used in conjunction with refCount, the source will be subscribed when the first subscriber registers and will automatically unsubscribe from the source when there are no more active subscribers. The multicasting behavior is achieved by placing a Subject(or any kind of subject) between the data consumers and the data producer.
() => subject implies that the same subject instance will be used every time the source will be subscribed, which is an important aspect as to why you're getting that behavior.
const src$ = (new BehaviorSubject(1)).pipe(
publishReplay(1), refCount() // 1 1
);
src$.pipe(take(1)).subscribe()
src$.subscribe(console.log)
Let's see what would be the flow of the above snippet:
src$.pipe(take(1)).subscribe()
Since it's the first subscriber, the source(the BehaviorSubject) will be subscribed. When this happens, it will emit 1, which will have to go through the ReplaySubject in use. Then, the subject will pass along that value to its subscribers(e.g take(1)). But because you're using publishReplay(1)(1 indicates the bufferSize), that value will be cached by that subject.
src$.subscribe(console.log)
The way refCount works is that it first subscribes to the Subject in use, and then to the source:
const refCounter = new RefCountSubscriber(subscriber, connectable);
// Subscribe to the subject in use
const subscription = connectable.subscribe(refCounter);
if (!refCounter.closed) {
// Subscribe to the source
(<any> refCounter).connection = connectable.connect();
}
Incidentally, here's what happens on connectable.subscribe:
_subscribe(subscriber: Subscriber<T>) {
return this.getSubject().subscribe(subscriber);
}
Since the subject is a ReplaySubject, it will send the cached values to its newly registered subscriber(hence the first 1). Then, because there were no subscribers before(due to take(1), which completes after the first emission), the source will be unsubscribed again, which should explain why you're getting the second 1.
If you'd like to get only one 1 value, you can achieve this by making sure that every time the source is subscribed, a different subject will be used:
const src$ = (new BehaviorSubject(1)).pipe(
shareReplay({ bufferSize:1, refCount: true }) // 1
);
src$.pipe(take(1)).subscribe()
src$.subscribe(console.log)
StackBlitz.

RxJS: Even if used `shareReplay()` on source observable, `throwError()` gets executed separately for each observable

I'm using RxJS shareReplay() operator on an observable (courses$) to share observable stream among other two observables (beginnerCourses$ and advancedCourses$). It's working fine and single API call response is shared between both observables on success.
But, when it comes to error, these observable don't share error and error is seen to be thrown twice in the browser console. Doesn't shareReplay() operator shares error also? Is it an intended behavior?
const http$ = createHttpObservable('/api/courses');
const courses$ = http$
.pipe(
map(res => res['payload'] ),
shareReplay(),
catchError(err => {
return throwError(err);
})
);
this.beginnerCourses$ = courses$
.pipe(
map(courses => courses
.filter(course => course.category === 'BEGINNER')));
this.advancedCourses$ = courses$
.pipe(
map(courses => courses
.filter(course => course.category === 'ADVANCED')));
}
It is expected behavior when an observable throws an error, shareReplay() will try resubscribe/re-execute the source
you can try this to validate
const a=defer(()=>{
console.log('run')
return throwError(new Error('Error'))
}).pipe(shareReplay())
a.subscribe(console.log,console.error,()=>console.log('complete'))
a.subscribe(console.log,console.error,()=>console.log('complete'))
https://stackblitz.com/edit/typescript-jutfxe
If you want observable to share the error without execute again
use publishReplay(1),refCount() instead
I think it's the expected behavior, what's a bit unexpected is that you're getting 2 different errors.
shareReplay places a ReplaySubject between the data consumer(s) and the data producer. When an error notification arrives, the ReplaySubject in use will send the same error notification to all the registered subscribers:
error(err: any) {
if (this.closed) {
throw new ObjectUnsubscribedError();
}
this.hasError = true;
this.thrownError = err;
this.isStopped = true;
const { observers } = this;
const len = observers.length;
const copy = observers.slice();
for (let i = 0; i < len; i++) {
copy[i].error(err);
}
this.observers.length = 0;
}
Source
but when using shareReplay, when an error occurs the ReplaySubject in use will be replaced by another one when a new subscriber is about to subscribe. Saying that it's being replaced, this also entails re-subscribing to the source.
So I think all the subscribers should receive the same error notification as long as they are already part of the ReplaySubject's list of subscribers. Otherwise, when a new subscriber comes in, the source will be re-subscribed.
What you could do is to prevent the ReplaySubject from receiving error notifications and allow its subscribers to receive it as they are, is to use materialize and dematerialize operators:
const courses$ = http$
.pipe(
materialize(), // Everything as a `next` notification
map(res => res['payload'] ),
shareReplay(),
dematerialize() // Back to the original event
);
With this approach, if a registered subscriber receives the error notification, it will be unsubscribed, meaning that it will also be removed from the ReplaySubject's list of subscribers. But the ReplaySubject will still be there and will not be replaced on subsequent subscribers.
Also, I think this is pretty redundant:
catchError(err => throwError(err));

Should I unsubscribe after a complete?

I have a quick question about observable.
I have the following observable:
getElevation(pos: Cartographic): Observable<Cartographic> {
return new Observable(observer => {
const promise = Cesium.sampleTerrain(this.terrainProvider, 11, Cesium.Cartographic(pos.longitude, pos.latitude))
Cesium.when(promise, (updatedPositions) => {
observer.next(updatedPositions);
observer.complete();
});
});
}
In a component I have:
this.service.getElevation(value).subscribe((e) => {});
My question is, this is a one shoot observable, so I complete just after, is the complete automatically close the subscription? or, do I also have to do this:
const sub = this.service.getElevation(value).subscribe((e) => {sub.unsubscribe();});
In your case you don't need to unsubscribe.
All Observers will automatically be unsubscribed when you call complete. That said, you may want to implement your consuming (component) code do handle the possibility that the implementation of the service may change in the future.
You could do this by using the take operator which will unsubscribe after the first value is emitted:
this.service.getElevation(value).pipe(take(1)).subscribe((e) => {});
You should not unsubscribe in a subscription, it the observable emits instantly then sub is undefined.
If you want a self unsubscribing observable you can use takeUntil
finalise = new Subject();
this.service.getElevation(value).pipe(takeUntil(finalise)).subscribe((e) => {
finalise.next();
finalise.complete();
});
Brief note:
Try to control the subscription with operators such as takeUntil.
You don’t need to unsubscribe yourself if the sender(Subject) completes.
For your case, since the sender returned by getElevation function completes itself after emitting a value one time, you don’t need to either use any operator or unsubscribe yourself to unsubscribe it.
All you have to do: this.service.getElevation(value).subscribe((v) => // do what you want);

rxjs, forkJoin(array).mergeMap(...), inner mergeMap never get called

In my case there are multiple requests could be performed in parallel at first, after those requests complete, another request will be sent with previous result, the pseudo code would look like
let uploads$ = [obs1$, obs2$, obs3$];
Observable.forkJoin(uploads$).mergeMap(
res => {
// never get called if uploads$ = []
let data = someCalculation(res);
return this.http.post('http://endpoint/api/resource', data);
}
).subscribe(
res => {
}
);
If uploads$ = [], the inner mergeMap never got called.
Can someone help? I'm on RxJS 5.4
It's not called b/c there is no emission on the source observable. To create one on, if observables is empty you can use the defaultIfEmpty or toArray operators.
const observables = [];
Rx.Observable.forkJoin(observables)
.defaultIfEmpty([]) // or .toArray()
.mergeMap(results => Rx.Observable.of(results.length))
.subscribe(console.log);

Resources