rxjs share with interval causes issue when waiting for next interval iteration - rxjs

I'm new to RxJs and need help/understanding for the following.
I have page that displays current covid cases. I have it setup to poll every 60 seconds. What I'm trying to understand is, if I subscribe to this observable via another new component, I have wait until the next iteration of 60 seconds is complete to get the data. My question is, if I want to share, is there any way to force to send the data and restart the timer?
I don't want 2 different 60 second intervals calling the API. I just want one, and the interval to restart if a new subscriber is initialized. Hope that makes sense.
this.covidCases$ = timer(1, 60000).pipe(
switchMap(() =>
this.covidService.getCovidCases().pipe(
map(data => {
return data.cases;
}),
),
),
retry(),
share(),
);

I think this should work:
const newSubscription = new Subject();
const covidCases$ = interval(60 * 1000).pipe(
takeUntil(newSubscription),
repeat(),
switchMap(() =>
this.covidService.getCovidCases().pipe(
/* ... */
),
),
takeUntil(this.stopPolling),
shareReplay(1),
src$ => defer(() => (newSubscription.next(), src$))
);
I replaced timer(1, 60 * 1000) + retry() with interval(60 * 1000).
My reasoning was that in order to restart the timer(the interval()), we must re-subscribe to it. But before re-subscribing, we should first unsubscribed from it.
So this is what these lines do:
interval(60 * 1000).pipe(
takeUntil(newSubscription),
repeat(),
/* ... */
)
We have a timer going on, until newSubscription emits. When that happens, takeUntil will emit a complete notification, then it will unsubscribe from its source(the source produced by interval in this case).
repeat will intercept that complete notification, and will re-subscribe to the source observable(source = interval().pipe(takeUntil())), meaning that the timer will restart.
shareReplay(1) makes sure that a new subscriber will receive the latest emitted value.
Then, placing src$ => defer(() => (newSubscription.next(), src$)) after shareReplay is very important. By using defer(), we are able to determine the moment when a new subscriber arrives.
If you were to put src$ => defer(() => (console.log('sub'), src$)) above shareReplay(1), you should see sub executed logged only once, after the first subscriber is created.
By putting it below shareReplay(1), you should see that message logged every time a subscriber is created.
Back to our example, when a new subscriber is registered, newSubscription will emit, meaning that the timer will be restarted, but because we're also using repeat, the complete notification won't be passed along to shareReplay, unless stopPolling emits.
StackBlitz demo.

This code creates an observable onject. I think what you should do is to add a Replaysubject instead of the Observable.
Replaysubjects gives the possibility to emit the same event when a new subscription occurs.
timer(1, 60000).pipe(
switchMap(() =>
this.covidService.getCovidCases().pipe(
tap(result => {
if (!result.page.totalElements) {
this.stopPolling.next();
}
}),
map(data => {
return data.cases;
}),
tap(results =>
results.sort(
(a, b) =>
new Date(b.covidDateTime).getTime() -
new Date(a.covidDateTime).getTime(),
),
),
),
),
retry(),
share(),
takeUntil(this.stopPolling),
).subscribe((val)=>{this.covidcases.next(val)});
This modification results in creating the timer once so when you subscribe to the subject it will emit the latest value immediately

You can write an operator that pushes the number of newly added subscriber to an given subject:
const { Subject, timer, Observable } = rxjs;
const { takeUntil, repeat, map, share } = rxjs.operators;
// Operator
function subscriberAdded (subscriberAdded$) {
let subscriberAddedCounter = 0;
return function (source$) {
return new Observable(subscriber => {
source$.subscribe(subscriber)
subscriberAddedCounter += 1;
subscriberAdded$.next(subscriberAddedCounter)
});
}
}
// Usage
const subscriberAdded$ = new Subject();
const covidCases$ = timer(1, 4000).pipe(
takeUntil(subscriberAdded$),
repeat(),
map(() => 'testValue'),
share(),
subscriberAdded(subscriberAdded$)
)
covidCases$.subscribe(v => console.info('subscribe 1: ', v));
setTimeout(() => covidCases$.subscribe(v => console.info('subscribe 2: ', v)), 5000);
subscriberAdded$.subscribe(v => console.warn('subscriber added: ', v));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>
Future possibilities:
You can update the operator easily to decrease the number in case you want to react on unsubscribers
!important
The takeUnit + repeat has already been postet by #AndreiGătej. I only provided an alternative way for receiving an event when a subscriber is added.
Running stackblitz with typescript
If the subscriberAdded operator needs some adjustements, please let me know and I will update

Related

Filtered send queue in rxjs

So I'm relatively inexperienced with rxjs so if this is something that would be a pain or really awkward to do, please tell me and I'll go a different route. So in this particular use case, I was to queue up updates to send to the server, but if there's an update "in flight" I want to only keep the latest item which will be sent when the current in flight request completes.
I am kind of at a loss of where to start honestly. It seems like this would be either a buffer type operator and/or a concat map.
Here's what I would expect to happen:
const updateQueue$ = new Subject<ISettings>()
function sendToServer (settings: ISettings): Observable {...}
...
// we should send this immediately because there's nothing in-flight
updateQueue$.next({ volume: 25 });
updateQueue$.next({ volume: 30 });
updateQueue$.next({ volume: 50 });
updateQueue$.next({ volume: 65 });
// lets assume that our our original update just completed
// I would now expect a new request to go out with `{ volume: 65 }` and the previous two to be ignored.
I think you can achieve what you want with this:
const allowNext$ = new Subject<boolean>()
const updateQueue$ = new Subject<ISettings>()
function sendToServer (settings: ISettings): Observable { ... }
updateQueue$
.pipe(
// Pass along flag to mark the first emitted value
map((value, index) => {
const isFirstValue = index === 0
return { value, isFirstValue }
}),
// Allow the first value through immediately
// Debounce the rest until subject emits
debounce(({ isFirstValue }) => isFirstValue ? of(true) : allowNext$),
// Send network request
switchMap(({ value }) => sendToServer(value)),
// Push to subject to allow next debounced value through
tap(() => allowNext$.next(true))
)
.subscribe(response => {
...
})
This is a pretty interesting question.
If you did not have the requirement of issuing the last in the queue, but simply ignoring all requests of update until the one on the fly completes, than you would simply have to use exhaustMap operator.
But the fact that you want to ignore all BUT the last request for update makes the potential solution a bit more complex.
If I understand the problem well, I would proceed as follows.
First of all I would define 2 Subjects, one that emits the values for the update operation (i.e. the one you have already defined) and one dedicated to emit only the last one in the queue if there is one.
The code would look like this
let lastUpdate: ISettings;
const _updateQueue$ = new Subject<ISettings>();
const updateQueue$ = _updateQueue$
.asObservable()
.pipe(tap(settings => (lastUpdate = settings)));
const _lastUpdate$ = new Subject<ISettings>();
const lastUpdate$ = _lastUpdate$.asObservable().pipe(
tap(() => (lastUpdate = null)),
delay(0)
);
Then I would merge the 2 Observables to obtain the stream you are looking for, like this
merge(updateQueue$, lastUpdate$)
.pipe(
exhaustMap(settings => sendToServer(settings))
)
.subscribe({
next: res => {
// do something with the response
if (lastUpdate) {
// emit only if there is a new "last one" in the queue
_lastUpdate$.next(lastUpdate);
}
},
});
You may notice that the variable lastUpdate is used to control that the last update in the queue is used only once.

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.

How to start processing after the user finishes typing with RxJS?

I have an input element, where I want to show an autocomplete solution. I try to control the HTTP request's number with RxJS. I want to do: the HTTP request start only after 1 second of the user stop the typing.
I have this code:
of(this.inputBox.nativeElement.value)
.pipe(
take(1),
map((e: any) => this.inputBox.nativeElement.value),
// wait 1 second to start
debounceTime(1000),
// if value is the same, ignore
distinctUntilChanged(),
// start connection
switchMap(term => this.autocompleteService.search({
term: this.inputBox.nativeElement.value
})),
).subscribe((result: AutocompleteResult[]) => {
console.log(result);
});
The problem is the debounceTime(1000) didn't wait to continue the pipe. The switchMap start immediately after every keyup event.
How can I wait 1 second after the user finishes typing?
The problem is that your chain starts with of(this.inputBox.nativeElement.value).pipe(take(1), ...) so it looks like you're creating a new chain (with a new debounce timer) on every single key press.
Instead you should have just one chain and push values to its source:
const keyPress$ = new Subject();
...
keyPress$
.pipe(
debounceTime(1000),
...
)
...
keyPress$.next(this.inputBox.nativeElement.value);
Why don't you create a stream with fromEvent?
I didn't find necessary to use distinctUntiChanged as the input event only triggers when a change occurs (i.e. the user adds/removes stuff). So text going through the stream is always different.
const {fromEvent} = rxjs;
const {debounceTime, map} = rxjs.operators;
const text$ =
fromEvent(document.querySelector('input'), 'input')
.pipe(
debounceTime(1000),
map(ev => ev.target.value));
text$.subscribe(txt => {
console.log(txt);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.4/rxjs.umd.min.js"></script>
<input/>

Implement a loop logic within an rxjs pipe

I have a class, QueueManager, which manages some queues.
QueueManager offers 3 APIs
deleteQueue(queueName: string): Observable<void>
createQueue(queueName: string): Observable<string>
listQueues(): Observable<string>: Observable`
deleteQueue is a fire-and-forget API, in the sense that it does not return any signal when it has completed its work and deleted the queue. At the same time createQueue fails if a queue with the same name already exists.
listQueues() returns the names of the queues managed by QueueManager.
I need to create a piece of logic which deletes a queue and recreates it. So my idea is to do something like
call the deleteQueue(queueName) method
start a loop calling the listQueues method until the result returned shows that queueName is not there any more
call createQueue(queueName)
I do not think I can use retry or repeat operators since they resubscribe to the source, which in this case would mean to issue the deleteQueue command more than once, which is something I need to avoid.
So what I have thought to do is something like
deleteQueue(queueName).pipe(
map(() => [queueName]),
expand(queuesToDelete => {
return listQueues().pipe(delay(100)) // 100 ms of delay between checks
}),
filter(queues => !queues.includes(queueName)),
first() // to close the stream when the queue to cancel is not present any more in the list
)
This logic seems actually to work, but looks to me a bit clumsy. Is there a more elegant way to address this problem?
The line map(() => [queueName]) is needed because expand also emits values from its source observable, but I don't think that's obvious from just looking at it.
You can use repeat, you just need to subscribe to the listQueues observable, rather than deleteQueue.
I've also put the delay before listQueues, otherwise you're waiting to emit a value that's already returned from the API.
const { timer, concat, operators } = rxjs;
const { tap, delay, filter, first, mapTo, concatMap, repeat } = operators;
const queueName = 'A';
const deleteQueue = (queueName) => timer(100);
const listQueues = () => concat(
timer(1000).pipe(mapTo(['A', 'B'])),
timer(1000).pipe(mapTo(['A', 'B'])),
timer(1000).pipe(mapTo(['B'])),
);
const source = deleteQueue(queueName).pipe(
tap(() => console.log('queue deleted')),
concatMap(() =>
timer(100).pipe(
concatMap(listQueues),
tap(queues => console.log('queues', queues)),
repeat(),
filter(queues => !queues.includes(queueName)),
first()
)
)
);
source.subscribe(x => console.log('next', x), e => console.error(e), () => console.log('complete'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.4/rxjs.umd.js"></script>

Change dynamically the concurrency factor in mergeMap operator

I have an Observable, mySourceObs, and the following code
concurrencyFactor = 10;
mySourceObs.pipe(
mergeMap(data => generateAnotherObservable(data), concurrencyFactor)
).subscribe();
Is there a way to change dynamically the concurrencyFactor once the chain of Observables has been subscribed?
You can do it like this, but inner observable will restart when concurrencyFactor changes.
concurrencyFactor = new BehaviorSubject(10);
combineLatest(mySourceObs,concurrent).pipe(
switchMap(([data,concurrent]) => generateAnotherObservable(data), concurrent)
).subscribe();
concurrencyFactor.next(5)
I have figured out a way of controlling the concurrency of mergeMap with a combination of an external Subject, switchMap and defer.
This is the sample code
// when this Observable emits, the concurrency is recalculated
const mySwitch = new Subject<number>();
let myConcurrency = 1;
// this block emits five times, every time setting a new value for the concurrency
// and the making mySwitch emit
merge(interval(1000).pipe(map(i => i + 2)), of(1))
.pipe(
delay(0),
take(5),
tap(i => {
myConcurrency = i * 2;
console.log("switch emitted", i);
mySwitch.next(i);
})
)
.subscribe();
// source is an hot Observable which emits an incresing number every 100 ms
const source = new Subject<number>();
interval(100)
.pipe(take(100))
.subscribe(i => source.next(i));
// this is the core of the logic
// every time mySwitch emits the old subscription is cancelled and a new one is created
mySwitch
.pipe(
switchMap(() =>
// defer is critical here since we want a new Observable to be generated at subscription time
// so that the new concurrency value is actually used
defer(() =>
source.pipe(
mergeMap(
i =>
// this is just a simulation of a an inner Observable emitting something
interval(100).pipe(
delay(100),
map(j => `Observable of ${i} and ${j}`),
take(10)
),
myConcurrency
)
)
)
)
)
.subscribe({
next: data => console.log(data)
});

Resources