Do something if RxJs subject's refCount drops to zero - rxjs

I'm working on a service layer that manages subscriptions.
I provide subject-backed observables to consumers like this:
const subject = new Subject();
_trackedSubjects.push(subject);
return subject.asObservable();
Different consumers may monitor the channel, so there may be several observables attached to each subject.
I'd like to monitor the count of subject.observers and if it ever drops back to 0, do some cleanup in my library.
I have looked at refCount, but this only is available on Observable.
I'd love to find something like:
subject.onObserverCountChange((cur, prev) =>
if(cur === 0 && prev !== 0) { cleanUp(subject) }
)
Is there a way to automatic cleanup like this on a subject?

Instead of using Subject - you should probably describe setup/cleanup logic when creating observable. See the example:
const { Observable } = rxjs; // = require("rxjs")
const { share } = rxjs.operators; // = require("rxjs/operators")
const eventSource$ = Observable.create(o => {
console.log('setup');
let i = 0
const interval = setInterval(
() => o.next(i++),
1000
);
return () => {
console.log('cleanup');
clearInterval(interval);
}
});
const events$ = eventSource$.pipe(share());
const first = events$.subscribe(e => console.log('first: ', e));
const second = events$.subscribe(e => console.log('second: ', e));
setTimeout(() => first.unsubscribe(), 3000);
setTimeout(() => second.unsubscribe(), 5000);
<script src="https://unpkg.com/rxjs#6.2.2/bundles/rxjs.umd.min.js"></script>

Related

How to do onething after another in rxjs

I find myself wanting to do this, which feels like it ought to be wrong.
this.isLoading = true;
this.service.getFirstValue().subscribe((response: firstValueType) => {
this.firstValue = response;
this.service.getSecondValue(this.firstValue).subscribe((response: secondValueType) => {
this.secondValue = response;
this.isLoading = false
});
});
What are you supposed to do?
you can use switchMap. Also embedded subscribe is bad practice, supposed to be avoided
this.isLoading = true;
this.service
.getFirstValue()
.pipe(
switchMap(response => {
this.firstValue = response;
return this.service.getSecondValue(this.firstValue);
})
)
.subscribe(response => {
this.secondValue = response;
this.isLoading = false;
});
PS fix the code, subscribe should be outside of pipe
The solution I figured out works fine for me in my project was using the following parts of rxjs:
ConnectableObservable
switchMap
tap
const { Subject } = rxjs;
const { publishReplay, tap, switchMapTo } = rxjs.operators;
// Simulates your service.getFirstValue, ...
const source1$$ = new Subject();
const source2$$ = new Subject();
const source3$$ = new Subject();
// Publish and Replay the last value to be able to connect (make it hot) and always get the lates value
const source1$ = source1$$.pipe(publishReplay(1));
const source2$ = source2$$.pipe(publishReplay(1));
const source3$ = source3$$.pipe(publishReplay(1));
// Connect to make the observable hot
source1$.connect()
source2$.connect()
source3$.connect()
source1$.pipe(
tap(v => console.log('tap source1$: ', v)),
switchMapTo(source2$),
tap(v => console.log('tap source2$: ', v)),
switchMapTo(source3$),
tap(v => console.log('tap source3$: ', v))
).subscribe()
source3$$.next('value 1');
source2$$.next('value 2');
source1$$.next('value 3');
source2$$.next('value 4');
source2$$.next('value 5');
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>
I am not sure if this fits your exactly requirements. But imo this is a at least close szenario like yours. If you want give me feedback and I try to adapt :)
Nested subscribe should be avoided.
In this case I would go like this
this.isLoading = true;
this.service.getFirstValue().pipe(
// this tap is necessary only if you use this.firstValue somewhere else in the code
// otherwise you can skip it and go directly to concatMap
tap((response_1: firstValueType) => this.firstValue = response_1),
// concatMap is usually the preferred operator to use when concatenating http calls
// read the article I have linked below
concatMap(response_1 => this.service.getSecondValue(response_1)),
tap(response_2 => {
this.secondValue = response_2;
this.isLoading = false;
})
).subscribe()
You can get some more inspiration on how to deal with http calls and rxJs reading this article.

Have multiple operations on a single event

Trying to have a event triggering multiple switchMap with the initial event data.
Each actions creates a promise to some transform that is then written in the file system. The actions are independent and unrelated, but uses the same data, just for different purpose, so they should not be merged.
Currently using taps instead of switchMap, that can lead to multiple event running the at the same time.
const SomeApiCall = () => {return {some: 'data'} }
const AllowsDoAction = () => {console.log('Parsing API and writting some things to FS -- PLACEHOLDER')}
const SomeTimeDoThisActionTo = () => {console.log('Parsing API and writting some other things to fs, by asking more data from the API and first, so it is long thing to do, so a new event can arrive first -- PLACEHOLDER')}
const deepEqual = (prev, cur) => prev === cur // normally a proper deepEqual...
const taps = [tap(AllowsDoAction)];
if (someCondition) taps.push(SomeTimeDoThisActionTo)
const observable = timer(0, 500).pipe(
exhaustMap(SomeApiCall),
distinctUntilChanged((prev, cur) => deepEqual(prev, cur))
...taps
);
I would return the filesystem write observables (edit 2)
const SomeApiCall = () => {
return of({ some: 'data' });
};
const AllowsDoAction = () => {
console.log('Parsing API and writting some things to FS -- PLACEHOLDER');
return timer(100).pipe(map(() => 'write 1 finished'));
};
const SomeTimeDoThisActionTo = () => {
console.log(
'Parsing API and writting some other things to fs, by asking more data from the API and first, so it is long thing to do, so a new event can arrive first -- PLACEHOLDER'
);
return timer(1000).pipe(map(() => 'write 2 finished'));
};
And then use concatMap to wait for all filesystem operation to complete.
const deepEqual = (prev, cur) => prev === cur; // normally a proper deepEqual...
const taps = [(AllowsDoAction)];
const someCondition = true;
if (someCondition) {
taps.push(SomeTimeDoThisActionTo);
}
const reduxStorageEvent$ = of('replace this with real event');
const observable = merge(timer(0, 500), reduxStorageEvent$).pipe(
exhaustMap(SomeApiCall),
distinctUntilChanged((prev, cur) => deepEqual(prev, cur)),
// use switchMap to cancel previous writes (edit 2)
// await latest write operations, before starting new writes
concatMap((someData) => {
const writes = taps.map((tapFx) => {
return tapFx(someData);
});
// wait for all writes
return forkJoin(...writes);
})
);
concatMap is like a queue. The first one in this queue has to finish before the second one can start.

RxJS: MergeMap with Preserving Input order

Requirement:
urls = [url1, url2, url3]
Fire all 3 urls parallely and paint the Dom in the sequnce of the urls list
ex: Finished order of urls = [url3, url1, url2]
when url1 finishes Immediately render the DOM, without waiting for url2
If url2, url3 finishes before url1, then store url2, url3 and paint the DOM after url1 arrives
Paint the DOM with order [url1, url2, url3]
My Work using promises:
// Fired all 3 urls at the same time
p1 = fetch(url1)
p2 = fetch(url2)
p3 = fetch(url3)
p1.then(updateDom)
.then(() => p2)
.then(updateDom)
.then(() => p3)
.then(updateDom)
I wanted to do the same thing in Observables.
from(urls)
.pipe(
mergeMap(x => fetch(x))
)
To fire them parallely I used merge map, but how can I order the sequence of the results?
The best way to preserve order with async tasks like this is with concatMap.
The problem is that if we apply this alone, we lose the parallelisation. If we were to do something like this:
from(urls)
.pipe(
concatMap(x => fetch(x))
);
the second request is not fired until the first is complete.
We can get around this by separating out the map into its own operator:
from(urls)
.pipe(
map(x => fetch(x)),
concatMap(x => x)
);
The requests will all be fired at the same time, but the results will be emitted in request order.
See Adrian's example adapted to use this approach below:
const { from } = rxjs;
const { concatMap, map } = rxjs.operators;
function delayPromise(value, delay) {
return new Promise(resolve => setTimeout(() => resolve(value), delay));
}
var delay = 3;
from([1, 2, 3]).pipe(
map(x => delayPromise(x, delay-- * 1000)),
concatMap(x => x)
).subscribe(result => { console.log(result); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>
I couldn't find anything that preserves the order so I came up with something a bit convoluted.
const { concat, of, BehaviorSubject, Subject } = rxjs;
const { delay, filter } = rxjs.operators;
const parallelExecute = (...obs$) => {
const subjects = obs$.map(o$ => {
const subject$ = new BehaviorSubject();
const sub = o$.subscribe(o => { subject$.next(o); });
return { sub: sub, obs$: subject$.pipe(filter(val => val)) };
});
const subject$ = new Subject();
sub(0);
function sub(index) {
const current = subjects[index];
current.obs$.subscribe(c => {
subject$.next(c);
current.obs$.complete();
current.sub.unsubscribe();
if (index < subjects.length -1) {
sub(index + 1);
} else {
subject$.complete();
}
});
}
return subject$;
}
parallelExecute(
of(1).pipe(delay(3000)),
of(2).pipe(delay(2000)),
of(3).pipe(delay(1000))
).subscribe(result => { console.log(result); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>
You can form a sequence with fetch and paint then forkJoin/Promise.all them
p1 = fetch(url1)
p2 = fetch(url2)
p3 = fetch(url3)
forkJoin(
from(p1).pipe(tap(_=>paint dom...))
from(p1).pipe(tap(_=>paint dom...))
from(p1).pipe(tap(_=>paint dom...))
).subscribe()

Check if publishReplay().refCount() has observers or not

I define an Observable like this:
const obs$ = Observable.create(...)
.publishReplay(1)
.refCount();
So that it puts a ReplaySubject(1) between my source Observable and all observers.
Since ReplaySubject has in its state the number of observers (via its observers array property), how is it possible to access the ReplaySubject from obs$?
I actually only need to know if obs$ has any observers or not. RxJS4 had a hasObservers() method on Subject, but it got removed in RxJS5. How can I achieve this with RxJS5?
Not sure about your usage but for my needs I created a custom operator that allowed me to transparently perform side-effects (similar to tap) based on the state of the refCount. It just does a pass-through subscription and duck-punches the sub/unsub. The callback gets the current refCount and the previous so that you can tell the state and direction. I like using an operator for this since I can insert it at any point in my stream. If you simply want a binary output for whether there are any subscriptions or not it could be easily modified for that.
const { Observable, Observer, interval } = rxjs;
const { publishReplay, refCount } = rxjs.operators;
const tapRefCount = (onChange) => (source) => {
let refCount = 0;
// mute the operator if it has nothing to do
if (typeof onChange !== 'function') {
return source;
}
// mute errors from side-effects
const safeOnChange = (refCount, prevRefCount) => {
try {
onChange(refCount, prevRefCount);
} catch (e) {
}
};
// spy on subscribe
return Observable.create((observer) => {
const subscription = source.subscribe(observer);
const prevRefCount = refCount;
refCount++;
safeOnChange(refCount, prevRefCount);
// spy on unsubscribe
return () => {
subscription.unsubscribe();
const prevRefCount = refCount;
refCount--;
safeOnChange(refCount, prevRefCount);
};
});
};
const source = interval(1000).pipe(
publishReplay(1),
refCount(),
tapRefCount((refCount, prevRefCount) => { console.log('refCount', refCount, prevRefCount > refCount ? 'down': 'up'); })
);
const firstSub = source.subscribe((x) => { console.log('first', x); });
let secondSub;
setTimeout(() => {
secondSub = source.subscribe((x) => { console.log('second', x); });
}, 1500);
setTimeout(() => {
firstSub.unsubscribe();
}, 4500);
setTimeout(() => {
secondSub.unsubscribe();
}, 5500);
<script src="https://unpkg.com/rxjs#rc/bundles/rxjs.umd.min.js"></script>
The typescript version:
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
export const tapRefCount = (
onChange: (refCount: number, prevRefCount: number) => void
) => <T>(source: Observable<T>): Observable<T> => {
let refCount = 0;
// mute the operator if it has nothing to do
if (typeof onChange !== 'function') {
return source;
}
// mute errors from side-effects
const safeOnChange = (refCount, prevRefCount) => {
try {
onChange(refCount, prevRefCount);
} catch (e) {
}
};
// spy on subscribe
return Observable.create((observer: Observer<T>) => {
const subscription = source.subscribe(observer);
const prevRefCount = refCount;
refCount++;
safeOnChange(refCount, prevRefCount);
// spy on unsubscribe
return () => {
subscription.unsubscribe();
const prevRefCount = refCount;
refCount--;
safeOnChange(refCount, prevRefCount);
};
}) as Observable<T>;
};
The Subject class has a public property called observers (see https://github.com/ReactiveX/rxjs/blob/5.5.10/src/Subject.ts#L28)
So you can use just:
const s = new Subject();
...
if (s.observers.length > 0) {
// whatever
}
Be aware that refCount returns an Observable so you won't be able to do what I mentioned above. However, you can provide your own Subject instance to publishReplay as the third argument and use s.observers on that, see http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-publishReplay

Create an Rx.Subject using Subject.create that allows onNext without subscription

When creating an Rx.Subject using Subject.create(observer, observable), the Subject is so lazy. When I try to use subject.onNext without having a subscription, it doesn't pass messages on. If I subject.subscribe() first, I can use onNext immediately after.
Let's say I have an Observer, created like so:
function createObserver(socket) {
return Observer.create(msg => {
socket.send(msg);
}, err => {
console.error(err);
}, () => {
socket.removeAllListeners();
socket.close();
});
}
Then, I create an Observable that accepts messages:
function createObservable(socket) {
return Observable.fromEvent(socket, 'message')
.map(msg => {
// Trim out unnecessary data for subscribers
delete msg.blobs;
// Deep freeze the message
Object.freeze(msg);
return msg;
})
.publish()
.refCount();
}
The subject is created using these two functions.
observer = createObserver(socket);
observable = createObservable(socket);
subject = Subject.create(observer, observable);
With this setup, I'm not able to subject.onNext immediately (even if I don't care about subscribing). Is this by design? What's a good workaround?
These are actually TCP sockets, which is why I haven't relied on the super slick websocket subjects.
The basic solution, caching nexts before subscription with ReplaySubject:
I think all you wanted to do is use a ReplaySubject as your observer.
const { Observable, Subject, ReplaySubject } = Rx;
const replay = new ReplaySubject();
const observable = Observable.create(observer => {
replay.subscribe(observer);
});
const mySubject = Subject.create(replay, observable);
mySubject.onNext(1);
mySubject.onNext(2);
mySubject.onNext(3);
mySubject.subscribe(x => console.log(x));
mySubject.onNext(4);
mySubject.onNext(5);
Results in:
1
2
3
4
5
A socket implementation (example, don't use)
... but if you're looking at doing a Socket implementation, it gets a lot more complicated. Here is a working socket implementation, but I don't recommend you use it. Rather, I'd suggest that you use one of the community supported implementations either in rxjs-dom (if you're an RxJS 4 or lower) or as part of RxJS 5, both of which I've helped work on.
function createSocketSubject(url) {
let replay = new ReplaySubject();
let socket;
const observable = Observable.create(observer => {
socket = new WebSocket(url);
socket.onmessage = (e) => {
observer.onNext(e);
};
socket.onerror = (e) => {
observer.onError(e);
};
socket.onclose = (e) => {
if (e.wasClean) {
observer.onCompleted();
} else {
observer.onError(e);
}
}
let sub;
socket.onopen = () => {
sub = replay.subscribe(x => socket.send(x));
};
return () => {
socket && socket.readyState === 1 && socket.close();
sub && sub.dispose();
}
});
return Subject.create(replay, observable);
}
const socket = createSocketSubject('ws://echo.websocket.org');
socket.onNext('one');
socket.onNext('two');
socket.subscribe(x => console.log('response: ' + x.data));
socket.onNext('three');
socket.onNext('four');
Here's the obligatory JsBin

Resources