How can we transform a Subject into an Observable in RxJs 5 ? This functionality is useful for example when we want to expose the Subject for subscription but don't want to yield control of calling next() on it, and prefer to keep the issuing of new values private.
The docs (see here) mention something like this:
var subject = new Rx.Subject();
var obs = subject.asObservable();
But in RxJs 5 this currently does not work (alpha 8), we get the following error:
"TypeError: subject.asObservable is not a function
FYI: Subject.prototype.asObservable() will be in the next release 5.0.0-beta.2.
At that point, you can just call mySubject.asObservable().
RxJS 5 is a rewrite (currently in beta) and has lots of renamed/removed methods, among other changes. One of the removed ones is asObservable.
You can get the same functionality by creating an observable with the private subject's subscribe function:
const subj = new rx.Subject();
const exposed = new rx.Observable(fn => subj.subscribe(fn));
Related
I need to create an event stream with all the events of my application in it, I need that stream to be replayable and to be able (if needed) to broadcast the old events to later initialized services.
So I've tried using the ReplaySubject, but then don't know the operator to avoid replaying if necessary:
import {ReplaySubject, of} from 'rxjs'
import {combineLatest} from 'rxjs/operators'
const bus$ = new ReplaySubject
const one$ = of(1)
// later
bus$.next('foo')
bus$.next('bar')
bus$.next('baz')
// Then I want to subscribe and know about the older events also
bus$.subscribe(doSomething)
// Or I just want the latest values from the subject
bus$
.pipe(
combineLatest(one$),
// take only the real value from the bus
map(x => x[0]),
)
.subscribe(doSomething)
I've found only this contrived solution... I guess there is an operator for that, or I'm simply wrong with my solution and there is a complete different approach...
Thanks in advance!
Update
Playing around with rxjs 6 I've refined my current solution:
import {ReplaySubject, EMPTY} from 'rxjs'
import {combineLatest} from 'rxjs/operators'
import {unary} from 'ramda'
const eventsBus = new ReplaySubject()
export const events$ = eventsBus
export const latestEvents$ = events$.pipe(combineLatest(EMPTY, unary))
Im guessing what you are looking for is the withLatestFrom operator.
https://www.learnrxjs.io/operators/combination/withlatestfrom.html
Sample:
https://stackblitz.com/edit/angular-pym83y
I'm creating my source observable like this (make api call every 5s):
const obs$ = Observable.interval(5000).switchMap(() => makeApiCall());
And I want to modify $obs so that it has the following characteristics:
start the observable only when at there's at least 1 subscriber
multicast. I.e. if I obs$.subscribe(...) twice, the underlying code makeApiCall() should only run once.
any subscriber which subscribes at any time should have immediately the last emitted value (and not wait ~5s until the next value emits)
retryable. If one makeApiCall() errors, I want (if possible) all subscribers to get an error notification, but reconnect to $obs, and continue doing makeApiCall() every 5s
So far I found the following leads:
It seems like I'd need to create a BehaviorSubject myBehaviorSubject, do a single subscription obs$.subscribe(myBehaviorSubject), and any other observers should subscribe to myBehaviorSubject. Not sure if that answers the "retryable" part.
I also looked at shareReplay, seems like $obs.shareReplay(1) would do the trick (for the 4 requirements). If I understood correctly it subscribes a ReplaySubject(1) to the source observable, and future observers subscribe to this ReplaySubject. Is there an equivalent shareBehavior?
In RxSwift, I found shareReplayLatestWhileConnected, which seems like the shareBehavior I was imagining. But it doesn't exist in RxJS.
Any ideas what is the best way to achieve this?
As you mentioned, shareReplay(1) pretty much gets you there. It will multicast the response to current subscribers and replay the last value (if there is one) to new subscribers. That seems like what you would want rather than shareBehavior (if it existed) since you are calling an api and there isn't an initial value.
You should know that shareReplay will create a subscription to the source stream but will only unsubscribe when refCount === 0 AND the source stream terminates (error or complete). This means that after the first subscription that the interval will start and even when there are no more subscriptions it will continue.
If you want to stop the interval when no-one is subscribed then use multicast(new ReplaySubject(1)).refCount(). The multicast operator will create a single subscription to the source stream and push all values into the subject provided as an instance (multicast(new Subject())) or by the factory (multicast(() => new Subject())). All subscribers to the stream after the multicast will subscribe to the multicast subject. So when a value flows through the multicast operator all of its subscribers will get that value. You can change the type of subject that you pass to multicast to change its behavior. In your case you probably want a ReplaySubject so that it will replay the last value to a new subscriber. You could use a BehaviorSubject too if you felt that met your need.
Now the multicast operator is connectable meaning that you would have to call connect() on the stream to make it hot. The refCount operator basically makes a connectable observable act like an ordinary observable in that it will become hot when subscribed but will become cold when there are no subscribers. It does this be keeping an internal reference count (hence the name refCount). When refCount === 0 it will disconnect.
This is the same thing as shareReplay(1) with one minor but important difference which is that when there are no more subscribers that it will unsubscribe from the source stream. If you are using a factory method to create a new subject when subscribing to the source (ex: multicast(() => new ReplaySubject(1))) then you will lose your value when the stream goes from hot to cold to hot since it will create a new subject each time it goes hot. If you want to keep the same subject between source subscriptions then you can pass in a subject instead of a factory (ex: multicast(new ReplaySubject(1)) or use its alias publishReplay(1).
As far as your last requirement of providing errors to your subscribers and then resubscribing, you can't call the error callback on a subscription and then continue getting values on the next callback. An unhandled error will end a subscription if it reaches it. So you have to catch it before it gets there and turn it into a normal message if you want your subscription to see it and still live. You can do this like so: catch((err) => of(err)) and just flag it somehow. If you want to mute it then return empty().
If you want to retry immediately then you could use the retryWhen operator but you probably want to put that before the sharing operator to make it universal. However this also prevents your subscribers from knowing about an error. Since the root of your stream is an interval and the error came from the inner observable returned from the switchMap, the error will not kill the source of the stream but it could kill the subscription. So as long as you handle the error (catch/catchError) the api call will be retried on the next interval.
Also, you may want timer(0, 5000) instead of interval so that your api call immediately fires and then fires on a 5 second interval after that.
So I would suggest something like the following:
let count = 0;
function makeApiCall() {
return Rx.Observable.of(count++).delay(1000);
}
const obs$ = Rx.Observable.timer(0, 5000)
.switchMap(() => makeApiCall().catch(() => Rx.Observable.empty()))
.publishReplay(1)
.refCount();
console.log('1 subscribe');
let firstSub = obs$.subscribe((x) => { console.log('1', x); });
let secondSub;
let thirdSub;
setTimeout(() => {
console.log('2 subscribe');
secondSub = obs$.subscribe((x) => { console.log('2', x); });
}, 7500);
setTimeout(() => {
console.log('1 unsubscribe');
firstSub.unsubscribe();
console.log('2 unsubscribe');
secondSub.unsubscribe();
}, 12000);
setTimeout(() => {
console.log('3 subscribe');
thirdSub = obs$.subscribe((x) => { console.log('3', x); });
}, 17000);
setTimeout(() => {
console.log('3 unsubscribe');
thirdSub.unsubscribe();
}, 30000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.10/Rx.min.js"></script>
For convenience, here are aliases for multicast:
publish() === multicast(new Subject())
publishReplay(#) === multicast(new ReplaySubject(#))
publishBehavior(value) === multicast(new BehaviorSubject(value))
I just tried to implement this with rxjs 6, but the implementation feels kinda hacky. I think there should be a much cleaner way to achieve this.
The expected behavior is:
As long as there are observers, they all get the same values.
When there are 0 observers, the source subscription is closed but the ReplaySubject is not completed.
When new observers subscribe again they get the last N values and a new subscription to source is established.
When the source completes or throws an error, current observers are completed resp. notified.
After source completion or source error, new subscribers don't get replayed values any more and are completed immediately.
export function shareReplayLatestWhileConnected<T>(count?: number) {
return function (source: Observable<T>): Observable<T> {
let done = false;
return source.pipe(
// Identify when source is completed or throws an error.
tap(
null,
() => (done = true),
() => (done = true),
),
multicast(
// Subject for multicasting
new ReplaySubject<T>(count),
// Selector function. Stop subscription on subject, when source is done, to kill all subscriptions.
(shared) => shared.pipe(takeWhile(() => !done)),
),
// I was not able to get rid of duplicate subscriptions. Multicast subscribed multiple times on the source.
share(),
);
};
}
Any tips on how I could improve this solution are very appreciated.
Use it like this:
const shared$ = source$.pipe(shareReplayLatestWhileConnected(1));
If new action is received by epic, stop processing old action(s).
A little bit of context: I have an epic, that emits few delayed actions.
I need to cancel them, if new action is received by epic.
Redux-saga effect that does exactly what I want:
takeLatest
That's exactly what .switchMap operator does:
Projects each source value to an Observable which is merged in the output
Observable, emitting values only from the most recently projected Observable.
In this example you can see that previous interval is no longer processing after new click arrives:
const clicks = Rx.Observable.fromEvent(document, 'click');
const result = clicks.switchMap((ev) => Rx.Observable.interval(1000));
result.subscribe(x => console.log(x));
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>
<button>send new click</button>
Here is what I'm doing now to convert an Observable to a ReplaySubject:
const subject = new Rx.ReplaySubject(1);
observable.subscribe(e => subject.next(e));
Is this the best way to make the conversion, or is there a more idiomatic way?
You can use just observable.subscribe(subject) if you want to pass all 3 types of notifications because a Subject already behaves like an observer. For example:
let subject = new ReplaySubject();
subject.subscribe(
val => console.log(val),
undefined,
() => console.log('completed')
);
Observable
.interval(500)
.take(5)
.subscribe(subject);
setTimeout(() => {
subject.next('Hello');
}, 1000)
See live demo: https://jsbin.com/bayewo/2/edit?js,console
However this has one important consequence. Since you've already subscribed to the source Observable you turned it from "cold" to "hot" (maybe it doesn't matter in your use-case).
It depends what do you mean by 'convert'.
If you need to make your observable shared and replay the values, use observable.pipe(shareReplay(1)).
If you want to have the subscriber functionality as well, you need to use the ReplaySubject subscribed to the original Observable observable.subscribe(subject);.
Like the first answer, as subject is also an observer.
const subject = new Rx.ReplaySubject(1);
observable.subscribe(subject);
I searched for the usage of defer in RxJS but still I don't understand why and when to use it.
As I understand neither Observable methods is fired before someone subscribes to it.
If that's the case then why do we need to wrap an Observable method with defer?
An example
I'm still wondering why it wrapped Observable with defer? Does it make any difference?
var source = Rx.Observable.defer(function () {
return Rx.Observable.return(42);
});
var subscription = source.subscribe(
function (x) { console.log('Next: ' + x); },
function (err) { console.log('Error: ' + err); },
function () { console.log('Completed'); } );
Quite simply, because Observables can encapsulate many different types of sources and those sources don't necessarily have to obey that interface. Some like Promises always attempt to eagerly compete.
Consider:
var promise = $.get('https://www.google.com');
The promise in this case is already executing before any handlers have been connected. If we want this to act more like an Observable then we need some way of deferring the creation of the promise until there is a subscription.
Hence we use defer to create a block that only gets executed when the resulting Observable is subscribed to.
Observable.defer(() => $.get('https://www.google.com'));
The above will not create the Promise until the Observable gets subscribed to and will thus behaves much more in line with the standard Observable interface.
Take for example (From this article):
const source = Observable.defer(() => Observable.of(
Math.floor(Math.random() * 100)
));
Why don't just set the source Observable to of(Math.floor(Math.random() * 100)?
Because if we do that the expression Math.floor(Math.random() * 100) will run right away and be available in source as a value before we subscribe to source.
We want to delay the evaluation of the expression so we wrap of in defer. Now the expression Math.floor(Math.random() * 100) will be evaluated when source is subscribed to and not any time earlier.
We are wrapping of(...) in the defer factory function such that the construction of of(...) happens when the source observable is subscribed to.
It would be easier to understand if we consider using dates.
const s1 = of(new Date()); //will capture current date time
const s2 = defer(() => of(new Date())); //will capture date time at the moment of subscription
For both observables (s1 and s2) we need to subscribe. But when s1 is subscribed, it will give the date-time at the moment when the constant was set. S2 will give the date-time at the moment of the subscription.
The code above was taken from https://www.learnrxjs.io/operators/creation/defer.html
An example, let's say you want to send a request to a server. You have 2 options.
Via XmlHttpRequest
if you do not subscribe to an existing Observable Observable.create(fn) there would not be any network request. It sends the request only when you subscribe. This is normal and as it should be via Observables. Its the main beauty of it.
Via Promise (fetch, rx.fromPromise)
When you use Promises it does not work that way. Whether you subscribed or not, it sends the network requests right away. To fix this, you need to wrap promises in defer(fn).
Actually you can fully replace defer with regular function. But you have to call the function before subscribing.
function createObservable() {
return from(fetch('https://...'));
}
createObservable().subscribe(...);
In case of defer you only need to pass createObservable function to defer.
Let's say you want to create an observable which when subscribed to, it performs an ajax request.
If you try the code below, the ajax request will be performed immediately,
and after 5 seconds the response object will be printed, which is not what you want.
const obs = from(fetch('http://jsonplaceholder.typicode.com/todos/1'));
setTimeout(()=>obs.subscribe((resp)=>console.log(resp)), 5000)
One solution is to manually create an Observable like below.
In this case the ajax response will be performed after 5 seconds (when subscribe() is called):
let obs = new Observable(observer => {
from(fetch('http://jsonplaceholder.typicode.com/todos/1')).subscribe(observer)
});
setTimeout(()=>obs.subscribe((resp)=>console.log(resp)), 5000)
defer achieves the above in a more straightforward way, and also without the need to use from() to convert promise to observable:
const obs = defer(()=>fetch('http://jsonplaceholder.typicode.com/todos/1'))
setTimeout(()=>obs.subscribe((resp)=>console.log(resp)), 5000)