Unsubscribing when using the shareReplay operator - rxjs

I have an observable that performs some relatively taxing work so I need to use the shareReplay(1) operator, it's important that I have access to an emitted value immediately as well so share won't quite achieve everything I need.
The issue is that when I'm trying to cleanup and unsubscribe the source observable will keep emitting, which I believe is caused by shareReplay keeping it alive due to refCount defaulting to false. This is also the behavior I need as at application start up the reference count will jump from 1 to 0 to 1 essentially, I don't want to observable restarting on a new subscription.
Is there any way to unsubscribe and stop the source observable emitting when using shareReplay? Seems a bit strange to have no way to clean up resources when using that particular operator.
A simple example:
const first = interval(10_000).pipe(
startWith(0),
withLatestFrom(someOtherObs),
map(([i, val]) => {
// some work
}),
shareReplay(1)
)
const second = interval(5_000).pipe(
withLatestFrom(first),
take(5)
)
const main = interval(5_000).pipe(
withLatestFrom(first),
map(([i, val]) => {
// perform work
})
)
const app = from([second, main]).pipe(concatAll()).subscribe()
Basically second will complete before main will start (hence the need for shareReplay), however it will continue emitting in perpetuity due to the interval... Is there anything I can do to avoid this while achieving the same behavior... Any help would be greatly appreciated.

In your case you'll need to force the source Observable of shareReplay(1) to complete for example with takeUntil() operator.
const done$ = new Subject();
const first = interval(10_000).pipe(
startWith(0),
withLatestFrom(someOtherObs),
map(([i, val]) => {
// some work
}),
takeUntil(done$),
shareReplay(1)
)
// ...
done$.next();

Related

DebounceTime after first value in RxJS

I need a specific behavior that I can't get with the RxJS operators. The closest would be to use DebounceTime only for values entered after the first one, but I can't find a way to do it. I have also tried with ThrottleTime but it is not exactly what I am looking for, since it launches intermediate calls, and I only want one at the beginning that is instantaneous, and another at the end, nothing else.
ThrottleTime
throttleTime(12 ticks, { leading: true, trailing: true })
source: --0--1-----2--3----4--5-6---7------------8-------9---------
throttle interval: --[~~~~~~~~~~~I~~~~~~~~~~~I~~~~~~~~~~~I~~~~~~~~~~~]--------
output: --0-----------3-----------6-----------7-----------9--------
source_2: --0--------1------------------2--------------3---4---------
throttle interval: --[~~~~~~~~~~~I~~~~~~~~~~~]---[~~~~~~~~~~~]--[~~~~~~~~~~~I~
output_2: --0-----------1---------------2--------------3-----------4-
DebounceTime
debounceTime(500)
source: --0--1--------3------------4-5-6-7-8-9-10-11--13----------------
debounce_interval: -----[~~~~~]--[~~~~~]--------------------------[~~~~~]----------
output: -----------1--------3--------------------------------13---------
What I want
debounceTimeAfterFirst(500) (?)
source: --0--1--------3------------4-5-6-7-8-9-10-11--13----------------
debounce_interval: -----[~~~~~]--[~~~~~]--------------------------[~~~~~]----------
output: --0--------1--3------------4-------------------------13---------
As you see, the debounce time is activated when a new value is entered. If the debounce time passes and any new value has been entered, it stops the listening the debounceTime action and waits to start a new one.
Edit: I forgot to comment that this must be integrated with NgRx’s Effects, so it must be a continuous stream that mustn't be completed. Terminating it would probably cause it to stop listening for dispatched actions.
I would use a throttle combined with a debounceTime:
throttle: from Documentation Emit value on the leading edge of an interval, but suppress new values until durationSelector has completed.
debounceTime: from Documentation Discard emitted values that take less than the specified time between output.
I would use a throttle stream to get the raising edge (the first emission) and then the debounce stream would give us the falling edge.
const source = fromEvent(document.getElementsByTagName('input'), 'keyup').pipe(
pluck('target', 'value')
);
const debounced = source.pipe(
debounceTime(4000),
map((v) => `[d] ${v}`)
);
const effect = merge(
source.pipe(
throttle((val) => debounced),
map((v) => `[t] ${v}`)
),
debounced
);
effect.subscribe(console.log);
See RxJS StackBlitz with the console open to see the values changing.
I prepared the setup to adapt it to NgRx which you mention. The effect I got working is:
#Injectable({ providedIn: 'root' })
export class FooEffects {
switchLight$ = createEffect(() => {
const source = this.actions$.pipe(
ofType('[App] Switch Light'),
pluck('onOrOff'),
share()
);
const debounced = source.pipe(debounceTime(1000), share());
return merge(source.pipe(throttle((val) => debounced)), debounced).pipe(
map((onOrOff) => SetLightStatus({ onOrOff }))
);
});
constructor(private actions$: Actions) {}
}
See NgRx StackBlitz with the proposed solution working in the context of an Angular NgRx application.
share: This operator prevents the downstream paths to simultaneously fetch the data from all the way up the chain, instead they grab it from the point where you place share.
I also tried to adapt #martin's connect() approach. But I don't know how #martin would "reset" the system so that after a long time if a new source value is emitted would not debounce it just in the same manner as you first run it, #martin, feel free to fork it and tweak it to make it work, I'm curious about your approach, which is very smart. I didn't know about connect().
#avicarpio give it a go on your application and let us know how it goes :)
I think you could do it like the following, even though I can't think of any easier solution right now (I'm assuming you're using RxJS 7+ with connect() operator):
connect(shared$ => shared$.pipe(
exhaustMap(value => merge(
of(value),
shared$.pipe(debounceTime(1000)),
).pipe(
take(2),
)),
)),
Live demo: https://stackblitz.com/edit/rxjs-qwoesj?devtoolsheight=60&file=index.ts
connect() will share the source Observable and lets you reuse it in its project function multiple times. I'm using it only to use the source Observable inside another chain.
exhaustMap() will ignore all next notifications until its inner Observable completes. In this case the inner Observable will immediately reemit the current value (of(value)) and then use debounceTime(). Any subsequent emission from source is ignored by exhaustMap() because the inner Observable hasn't completed yet but is also passed to debounceTime(). Then take(2) is used to complete the chain after debounceTime() emits and the whole process can repeat when source emits because exhaustMap() won't ignore the next notification (its inner Observable has completed).
Here's a custom operator that (as far s I can tell) does what you're after.
The two key insights here are:
Use connect so that you can subscribe to the source twice, once to ignore emissions with exhaustMap and another to inspect and debounce emissions with switchMap
Create an internal token so that you know when to exit without a debounced emission. (Insures that from your example above, the 4 is still emitted).
function throttleDebounceTime<T>(interval: number): MonoTypeOperatorFunction<T> {
// Use this token's memory address as a nominal token
const resetToken = {};
return connect(s$ => s$.pipe(
exhaustMap(a => s$.pipe(
startWith(resetToken),
switchMap(b => timer(interval).pipe(mapTo(b))),
take(1),
filter<T>(c => c !== resetToken),
startWith(a)
))
));
}
example:
of(1,2,3,4).pipe(
throttleDebounceTime(500)
).subscribe(console.log);
// 1 [...0.5s wait] 4

MergeMap from Array of Observables

TLDR: Working example is in the last codeblock of this question. Check out #bryan60 answer for a working example using concat rather than mergeMap.
I'm trying to run a number of remote requests sequentially, but only the first observable is executed.
The number of request vary, so I can't do a dodgy solution where I nest observables within each other.
I'm using the following code:
const observables = [
observable1,
observable2,
...
];
from(observables).pipe(
mergeMap(ob=> {
return ob.pipe(map(res => res));
}, undefined, 1)
).subscribe(res => {
console.log('Huzzah!');
})
In the past (rxjs 5.5) Ive used the following:
let o = Observable.from(observables).mergeMap((ob) => {
return ob;
}, null, 1);
o.subscribe(res => {
console.log('Huzzah!');
})
I'm not sure what I'm doing wrong, can anybody shed some light?
An additional request would be to only print 'Huzzah!' once on completion of all requests rather than for each individual Observable.
EDIT:
Removing undefined from my original code will make it work, however there was another issue causing only the first observable to be executed.
I'm using Angular's HttpClient for remote requests. My observable code looked like this:
const observables = [];
// Only the first observable would be executed
observables.push(this.http.get(urla));
observables.push(this.http.get(urlb));
observables.push(this.http.get(urlc));
Adding .pipe(take(1)) to each observable results in each observable being executed:
const observables = [];
// All observables will now be executed
observables.push(this.http.get(urla).pipe(take(1));
observables.push(this.http.get(urlb).pipe(take(1));
observables.push(this.http.get(urlc).pipe(take(1));
The code I ended up using, which executes all observables in sequential order and only triggers Huzzah! once is:
const observables = [];
observables.push(this.http.get(urla).pipe(take(1));
observables.push(this.http.get(urlb).pipe(take(1));
observables.push(this.http.get(urlc).pipe(take(1));
from(observables).pipe(
mergeMap(ob=> {
return ob.pipe(map(res => res));
}, 1),
reduce((all: any, res: any) => all.concat(res), [])
).subscribe(res => {
console.log('Huzzah!');
})
Thanks to #bryan60 for helping me wit this issue.
if these are http requests that complete, I think your bug is caused by a change to the mergeMap signature that removed the result selector. it's hard to be sure without knowing exactly which version you're on as it was there, then removed, then added again, and they're removing it once more for good in v7.
if you want to run them sequentially... this is all you need...
// concat runs input observables sequentially
concat(...observables).subscribe(res => console.log(res))
if you want to wait till they're all done to emit, do this:
concat(...observables).pipe(
// this will gather all responses and emit them all when they're done
reduce((all, res) => all.concat([res]), [])
// if you don't care about the responses, just use last()
).subscribe(allRes => console.log(allRes))
In my personal utility rxjs lib, I always include a concatJoin operator that combines concat and reduce like this.
the only trick is that concat requires observables to complete till it moves on to the next one, but the same is true for mergeMap with concurrent subscriptions set to 1.. so that should be fine. things like http requests are fine, as they complete naturally after one emission.. websockets or subjects or event emitters will behave a bit differently and have to be manually completed, either with operators like first or take or at the source.
If you are not concerned about the sequence of execution and just want 'Huzzah!' to be printed once all the observable has been executed forkJoin can also be used.Try this.
forkJoin(...observables).subscribe(res => console.log('Huzzah');

RxJs share operator and Observable created with range

I am trying to understand why share RxJs operator works differently if the source Observable is created with range instead of timer.
Changing the original code to:
const source = range(1, 1)
.pipe(
share()
)
const example = source.pipe(
tap(() => console.log('***SIDE EFFECT***')),
mapTo('***RESULT***'),
)
const sharedExample = example
const subscribeThree = sharedExample.subscribe(val => console.log(val))
const subscribeFour = sharedExample.subscribe(val => console.log(val))
Results in:
console.log src/pipeline/foo.spec.ts:223
SIDE EFFECT
console.log src/pipeline/foo.spec.ts:228
RESULT
console.log src/pipeline/foo.spec.ts:223
SIDE EFFECT
console.log src/pipeline/foo.spec.ts:229
RESULT
Basically, the side effect is invoked more than once.
As far as I know range is supposed to be a cold observable but it is said that share should turn cold observables to hot.
What is the explanation behind this behaviour ?
Two things to point out.
First, if you look closely at the function signature for range, you'll see it takes a third parameter, a SchedulerLike.
If unspecified, RxJS calls the next handler of each subscriber immediately with the relevant value for the range observable until it's exhausted. This isn't desirable if you intend to use the share operator, because it effectively bypasses any shared side effect processing that might be introduced.
Relevant snippet taken from the actual implementation:
// src/internal/observable/range.ts#L53
do {
if (index++ >= count) {
subscriber.complete();
break;
}
subscriber.next(current++);
if (subscriber.closed) {
break;
}
} while (true);
timer also takes an optional SchedulerLike argument. If unspecified, the implementation adopts AsyncScheduler by default, different to the default for range.
Secondly, the share operator should follow all other operators that might have side effects. If it precedes them, the expected unifying behaviour of pipe operator processing is lost.
So, with both points in mind, to make the share operator work with range as you're expecting:
const { asyncScheduler, range, timer } = rxjs;
const { mapTo, tap, share } = rxjs.operators;
// Pass in an `AsyncScheduler` to prevent immediate `next` handler calls
const source = range(1, 1, asyncScheduler).pipe(
tap(() => console.log('***SIDE EFFECT***')),
mapTo('***RESULT***'),
// All preceding operators will be in shared processing
share(),
);
const sub3 = source.subscribe(console.log);
const sub4 = source.subscribe(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>

RxJS v5 Pausable Observable Interval

With the pausable operator not implemented in RxJS v5, is there a better way to create a pausable interval? The code below works, but does so by keeping track of the last emitted value as an offset. It seems like there should be a better way...
const source = Rx.Observable.interval(100).share()
const offset = new Rx.BehaviorSubject(0)
let subscription;
let currentValue;
function start() {
subscription = source
.subscribe(i => {
currentValue = i + offset.value
})
}
function pause() {
source.take(1).subscribe(i => offset.next(i + offset.value))
subscription.unsubscribe()
}
The share() operator is an alias for .publish().refCount(). The refCount() means that the observable will clean itself up when no other subscribers to it exist. Because you're unsubscribing from source, it will clean itself up, then restart when subscribed to again. Use publish() with connect() instead. Here's the code:
const source = Observable.interval(100).publish();
source.connect();
// Start with false, change to true after 200ms, then false again
// after another 200ms
const pauser = Observable.timer(200)
.mapTo(true)
.concat(Observable.timer(200).mapTo(false))
.startWith(false);
const pausable = pauser
.switchMap(paused => (paused ? Observable.never() : source))
.take(10);
pausable.subscribe(x => console.log(x));
See this jsbin for a running example: http://jsbin.com/jomusiy/3/edit?js,console.
There's no general purpose way to do this. It depends on what exactly you mean by pausing and also what you're pausing. (Do you want to just stop emitting and then start again, do you buffer and drain, or do you effectively need to add a delay to the upstream, but otherwise preserve the distribution of upstream values in time?)
I have a way to do this in an efficient manner when the upstream is specifically a timer, as it is in your example. It is the answer to my own question here.
RxJS (5.0rc4): Pause and resume an interval timer
This one has the great advantage of preserving the source's value distribution in time, but just adding a delay to it.
For more general cases:
For cold observables: switch between a never and upstream. On pause, unsubscribe from upstream. skip ones that you've seen, and then switch to the skiped stream. While unpaused you must maintain a count of how many values you've emitted, so that you can skip that many the next time someone unpauses. You only have to remember how many you've seen before. However, every unpause causes the cold observable to replay from the beginning. This could be very inefficient in the general case. The code would look something like this. pauser here would be a Subject that you can set to true or false to pause upstream.
function pausableCold(pauser, upstream) {
var seen = 0;
return pauser.switch(paused => {
if (paused) {
return Observable.never();
}
else {
return upstream.skip(seen).do(() => seen++);
}
});
}
For hot or cold observables, you can use buffering. buffer while paused and then drain and concat to the hot upstream when unpaused. (This preserves all the values but but it doesn't preserve their distribution in time. Also, you should hot upstream with publish if it could be cold.)
The most efficient way is not really part of the Rx. What you really want is to tell the source to stop emitting and then start again. The way you do that is very specific to what the source is and how the source is generating values.

Debounce without initial delay

Is there an operator in RxJS that debounces without delaying the "first event in a burst", but delaying (and always emitting) the "last event in a burst"?
Something like this:
---a----b-c-d-----e-f---
after awesome-debounce(2 dashes) becomes:
---a----b------d--e----f
while a normal debounce would be:
-----a---------d-------f
It's kind of a mix between throttle and debounce...
Hmmm, this is the easiest solution I can think of. The interesting part for you is the awesomeDebounce() function that creates the sub-chain.
It basically just combines throttle() and debounceTime() operators:
const Rx = require('rxjs');
const chai = require('chai');
let scheduler = new Rx.TestScheduler((actual, expected) => {
chai.assert.deepEqual(actual, expected);
console.log(actual);
});
function awesomeDebounce(source, timeWindow = 1000, scheduler = Rx.Scheduler.async) {
let shared = source.share();
let notification = shared
.switchMap(val => Rx.Observable.of(val).delay(timeWindow, scheduler))
.publish();
notification.connect();
return shared
.throttle(() => notification)
.merge(shared.debounceTime(timeWindow, scheduler))
.distinctUntilChanged();
}
let sourceMarbles = '---a----b-c-d-----e-f---';
let expectedMarbles = '---a----b------d--e----f';
// Create the test Observable
let observable = scheduler
.createHotObservable(sourceMarbles)
.let(source => awesomeDebounce(source, 30, scheduler));
scheduler.expectObservable(observable).toBe(expectedMarbles);
scheduler.flush();
The inner notification Observable is used only for the throttle() operator so I can reset its timer manually when I need. I also had to turn this Observable into "hot" to be independent on the internal subscriptions from throttle().
That's indeed useful debounce type for many situations. Use merge, throttleTime and debounceTime in a next way:
Rx.Observable.merge(source.throttleTime(1000), source.debounceTime(2000))
Full example is here http://jsbin.com/godocuqiwo/edit?js,console
Note: it will emit not only first and last value in debounce interval but also values produced by throttle (which is usually expected and needed, as for scroll debouncing for example).

Resources