rxjs - Subject subscriber misses a value - rxjs

I have a Subject that is next'ed with a value before it has any subscribers - how do I make subscribers not miss values that got sent before the subscription?
Some code:
subject = new Subject<string>;
subject.next('value');
// at a later time
subject.subsribe(val => {...});

If you want a subject that will emit values to subscribers that subscriber after next has been called, you can use a ReplaySubject.
When creating a ReplaySubject, you can specify the number of next notifications that are to be replayed. To replay only one, you'd use:
subject = new ReplaySubject<string>(1);

Related

Propagate new Observable's value to some observers only

Suppose we have an Observable which is subscribed to by multiple observers.
When that Observable emits a new value, can we control which of its observers would get the new value?
I'm attempting to implement a scenario when observers are receiving the new value one by one and expected to return true or false in response to that value. When a first observer returns true, the rest of observers must not receive the new value.

shareReplayLatestWhileConnected with RxJS

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));

How to pause Observable?

I am working on my first Angular 2 product. I created this Observable.timer and then subscribed to it. It polls the server for data.
How do I pause and unpause this?
ngOnInit(): void {
this.poll = Observable.timer(500,500).subscribe((t: any) => {
this.request.getRackList(+this.global.zoneId).subscribe(
incoming => this.import(incoming),
err => { this.global.err(err) }
);
})
this.canOpenClose = (this.global.mode === 2);
}
There are multiple approaches to pause an Observable. You could:
Unsubscribe / resubscribe when needed
.filter() out emissions when paused
create a connectable Observable so you can pause / start emissions to all subscribers
You can have a look here : RxJS - pause, upon resume give last paused value
Basically, you use the switchMap operator to stop the timer emissions/restart them and a pauser subject to emit a boolean indicating whether you want to pause or restart.
If you don't need the latest value, you can replace publishReplay by publish

Choosing the right RxJS construct in order to ensure one subscription completes before another one

I am facing a RxJS issue.
My app is currently designed as follows: I have two different clients: ClientA & ClientB that subscribe to two different Observables: ObservableA & ObservableB.
Note that the app also mutates a variable called aVariable.
Here is the flow:
ClientA subscribes to ObservableA.
ClientB subscribes to ObservableB.
ObservableB subscription read false from aVariable and completes.
ObservableA subscription sets aVariable to true and completes (later than ObservableB).
Whereas what is really intended was for ObservableA's subscription to complete before ObservableB's so that ClientB would read true from aVariable... Or to put it another way, somehow ensure that ObservableB's subscription waits till the other subscription has completed.
I am not sure what RxJS construct to use in order to achieve what I want (I currently use plain Observables). I believe I need more than plain Observables here...
Can someone please help?
P.S. Note that aVariable is held in a ngrx store but I don't think that is relevant to this issue...
P.P.S. The above is a simplification of my real app.
I think you can solve your problem with a intermediate Subject in which you emit a value when streamB gets subscribed to:
const completeStreamA = new Rx.Subject();
const streamA = Rx.Observable.never()
.takeUntil(completeStreamA);
const streamB = Rx.Observable.of('aValueOnStreamB')
.do(() => completeStreamA.next('complete stream A'));
//clientA subscribes immediately
streamA.subscribe(
next => console.log('a->next->'+next),
err => console.log('a->error->' + err.message),
() => console.log('a->complete')
);
setTimeout(() => {
//simulate later subscription by clientB
streamB.subscribe(
next => console.log('b->next->'+next),
err => console.log('b->error->' + err.message),
() => console.log('b->complete')
);
}, 3 * 1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.3/Rx.js"></script>
Only after the streamB gets subscribed to it will next a value into the completeStreamA subject which will complete streamA. The output of above code:
a->complete
b->next->aValueOnStreamB
b->complete

Using Reactives to Merge Chunked Messages

So I'm attempting to use reactives to recompose chunked messages identified by ID and am having a problem terminating the final observable. I have a Message class which consists of Id, Total Size, Payload, Chunk Number and Type and have the following client-side code:
I need to calculate the number of messages to Take at runtime
(from messages in
(from messageArgs in Receive select Serializer.Deserialize<Message>(new MemoryStream(Encoding.UTF8.GetBytes(messageArgs.Message))))
group messages by messages.Id into grouped select grouped)
.Subscribe(g =>
{
var cache = new List<Message>();
g.TakeWhile((int) Math.Ceiling(MaxPayload/g.First().Size) < cache.Count)
.Subscribe(cache.Add,
_ => { /* Rebuild Message Parts From Cache */ });
});
First I create a grouped observable filtering messages by their unique ID and then I am trying to cache all messages in each group until I have collected them all, then I sort them and put them together. The above seems to block on g.First().
I need a way to calculate the number to take from the first (or any) of the messages that come through however am having difficulty doing so. Any help?
First is a blocking operator (how else can it return T and not IObservable<T>?)
I think using Scan (which builds an aggregate over time) could be what you need. Using Scan, you can hide the "state" of your message re-construction in a "builder" object.
MessageBuilder.IsComplete returns true when all the size of messages it has received reaches MaxPayload (or whatever your requirements are). MessageBuilder.Build() then returns the reconstructed message.
I've also moved your "message building" code into a SelectMany, which keeps the built messages within the monad.
(Apologies for reformatting the code into extension methods, I find it difficult to read/write mixed LINQ syntax)
Receive
.Select(messageArgs => Serializer.Deserialize<Message>(
new MemoryStream(Encoding.UTF8.GetBytes(messageArgs.Message))))
.GroupBy(message => message.Id)
.SelectMany(group =>
{
// Use the builder to "add" message parts to
return group.Scan(new MessageBuilder(), (builder, messagePart) =>
{
builder.AddPart(messagePart);
return builder;
})
.SkipWhile(builder => !builder.IsComplete)
.Select(builder => builder.Build());
})
.Subscribe(OnMessageReceived);

Resources