Hi there I need a variant of shareReplay() that cache events until I trigger a signal asking it to discard buffered events.
Is there an operator or a combination of operators that let me do that easily or should I build my own custom operator?
try the following code, switchMap can cancel the stream subscription and resubscribe which means shareReplay() will be a fresh one
const intercept = fromEvent(document, "click").pipe(startWith(true))
const cacheEvent=interval(2000).pipe(shareReplay(1))
const source = intercept.pipe(switchMapTo(cacheEvent))
Related
From the RxJS documentation I see the following example:
const source = interval(1000);
const clicks = fromEvent(document, 'click');
const result = source.pipe(takeUntil(clicks));
result.subscribe(x => console.log(x));
This is close to a code pattern needed for my app but I see a problem. The takeUntil operator subscribes, but as I understand it an Observer has no way to unsubscribe from the source Observable. It has no access to a Subscription object on which it can call unsubscribe().
So if I understand this correctly then once the user clicks the source observable will continue to emit ticks forever to the takeUntil which will consume them and do nothing with them.
Am I reading this correctly? If so is there a generally accepted way to kill the source observable from within the Observer pipe?
What happens with takeUntil is the following.
When the Observable passed to takeUntil as parameter notifies a value, the subscriber of the Observable returned by takeUntil completes and, as a consequence, all the subscriptions created in the pipe chain are unsubscribed one after the other in reverse order.
In simpler words, the unsubscription is performed behind the scene by the RxJs internal mechanisms.
To prove this behavior you can try this code
const source = interval(1000).pipe(
tap({ next: (val) => console.log('source value', val) })
);
const clicks = fromEvent(document, 'click');
const result = source.pipe(takeUntil(clicks));
result.subscribe((x) => console.log(x));
If you run it, you see that the message 'source value', val is printed until the click occurs. After this, no more message is printed on the console, which means that the Observable upstream, i.e. the Observable created by the interval function does not notify any more.
You can try the above code in this stackblitz.
SOME DETAILS ON THE INTERNALS
We can take a look at the internals of the RxJs implementation to see how this unsubscribe behind the scenes works.
Let's start from takeUntil. In its implementation we see a line like this
innerFrom(notifier).subscribe(new OperatorSubscriber(subscriber, () => subscriber.complete(), noop));
which, in essence, says that as soon as the notifier (i.e. the Observable passed to takeUntil as parameter) notifies, the complete method is called on the subscriber.
The invocation of the complete method triggers many things, but eventually it ends up calling the method execTeardown of Subscription which ends up invoking unsubscribe of OperatorSubscriber which itself calls unsubscribe of Subscription.
As we see, the chain is pretty long and complex to follow, but the core message is that the tearDown logic (i.e. the logic which is invoked when an Observable completes, errors or is unsubscribed) calls the unsubscription logic.
Maybe it is useful to look at one more thing, an implementation of a custom operator directly from the RxJs documentation.
In this case, at the end of the definition of the operator, we find this piece of code
// Return the teardown logic. This will be invoked when
// the result errors, completes, or is unsubscribed.
return () => {
subscription.unsubscribe();
// Clean up our timers.
for (const timerID of allTimerIDs) {
clearTimeout(timerID);
}
};
This is the teardown logic for this custom operator and such logic invokes the unsubscribe as well as any other cleanup activity.
I have a hot observable emitting messages continuously. I need to pause it with an API REST endpoint like an actuator /messages/pause, and resume it with other API REST endpoint like /messages/resume. During the pause time I need to allow with other API REST endpoint to emit a message like the original observable /messages/custom.
It is possible to pause the main observable stream during that interval (pause-resume) but not stop observing the mocked messages events, and continue/restore the main observable stream after resume it?
I think this would be an approach:
// The 2 sources of events.
const main$ = /* ... */;
const second$ = /* ... */;
// Emits when the `/pause` endpoint is reached.
// `pause.next(true);`
// When the `/resume` endpoint is reached: `pause.next(false)`
const pause = new Subject<boolean>();
const decoratedMain$ = main$.pipe(
withLatestFrom(paused.pipe(startWith(false))),
filter(([mainValue, isPaused]) => !isPaused),
map(([mainValue]) => mainValue),
);
const decoratedSecond$ = second$.pipe(
withLatestFrom(paused),
filter(([secondValue, isPaused]) => isPaused),
map(([secondValue]) => secondValue),
);
merge(decoratedMain$, decoratedSecond$).subscribe();
The above snippet, unless I've missed something, should implement this logic:
while the main$ observable emits, the second$ observable's events won't be taken into consideration
main$ can be paused(with pause.next(true)) and resumed(with pause.next(false))
when main$ is paused, its events are ignored and the second$ observable's events are now considered
when main$ is resumed, the switch takes place again: second$'s events are ignored and main$'s events are considered
Let's now see what part of RxJS' magic has been used to achieve this.
As you may have noticed, the main logic revolves around withLatestFrom and the pause Subject.
The decoratedMain$ observable keeps on emitting, but whether its events are ignored or not depends on the latest value of pause. If the /pause endpoint is reached, then the events will be ignored, since pause would have emitted true.
The decoratedSecond$ is built symmetrically. Its events are ignored if the latest value of pause is false.
Lastly, I thought it would be helpful to share a small variation of the above approach, just for learning purposes:
/* ... */
const isMainPaused$ = pause.pipe(filter(Boolean));
const isMainResumed$ = pause.pipe(filter(v => !v));
const decoratedMain$ = main$.pipe(
share({ resetOnRefCountZero: false }),
takeUntil(isMainPaused$),
repeatWhen(completionsSubject => completionsSubject.pipe(mergeMapTo(isMainResumed$)))
);
/* ... */
The rest of the code is the same as in the first approach. Here, what's going on is essentially ignoring the main$'s events by not having any subscriber to the share's Subject instance. When pause emits false, a new subscriber is created for that Subject instance, without re-subscribing to the source and that's due to the resetOnRefCountZero: false option.
I have the following operators:
const prepare = (value$: Observable<string>) =>
value$.pipe(tap((x) => console.log("remove: ", x)), share());
const performTaskA = (removed$: Observable<string>) =>
removed$.pipe(tap((x) => console.log("pathA: ", x)),);
const performTaskB = (removed$: Observable<string>) =>
removed$.pipe(tap((x) => console.log("pathB: ", x)));
and I call them like this:
const prepared$ = value$.pipe(prepare);
const taskADone$ = prepared$.pipe(performTaskA);
const taskBDone$ = prepared$.pipe(performTaskB);
merge(taskADone$, taskBDone$).subscribe();
Due to the share in prepare I would expect 'remove' to be logged only once, however it appears twice.
Why is this not working?
Codesandbox: https://codesandbox.io/s/so-remove-fires-twice-iyk12?file=/src/index.ts
This is happening because your source Observable is of() that just emits one next notification and then complete. Everything in RxJS in synchronous unless you work with time or you intentionally make your code asynchronous (eg. with Promise.resolve or with asyncScheduler).
In your demo, share() receives one next and one complete notification immediately which makes its internal state to reset. It will also unsubscribe from its source Obserable because there are no more observers (the second source taskBDone$ you're merging has not subscribed yet). Then taskBDone$ is merged into the chain and share() creates internally a new instance of Subject and the whole process repeats.
These are the relevant parts in share():
Dispose handler triggered after receiving complete from source https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/share.ts#L120
New Subject created: https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/share.ts#L113
share() resets its state https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/share.ts#L163
So if your sources are going to be synchronous you should better use shareReplay() (instead of share()) that will just replay the entire sequence of events to every new observer.
Your updated demo: https://stackblitz.com/edit/rxjs-jawajw?devtoolsheight=60
Notice, that in your demo if you used of("TEST").pipe(delay(0)) as your source Observable it would work as you expected because delay(0) would force asynchronous behavior and both source Observables would first subscribe and then in another JavaScript frame would emit their next and complete.
What's the best way to handle asynchronous updates in the middle of an Observable stream.
Let's say there are 3 observables:
Obs1 (gets data from API) -> pipes to Obs2
Obs2 (transforms data) -> pipes to Obs3
Obs3 (sends transformed data)
(The actual application is more complex, and there's reasons it's not done in a single Observable, this is just a simple example).
That all works well and good if it's a linear synchronous path.
But we also have async messages that will change the output of Obs2.
3 scenarios I'm asking about are:
- we fetch data, and go through Obs1, Obs2 & Obs3
- we get a message to make a change, go through Obs2 & Obs3
- we get a different message to make a change which also needs to apply the change from the previous message, through Obs2 & Obs3
The main problem here is that there are different types of asynchronous messages that will change the outcome of Obs2, but they all need to still know what the previous outcome of Obs2 was (so the any other changes from messages that happened before is still applied)
I have tried using switchMap in Obs2 with a scan in Obs1 like this:
obs1
const obs1$ = obs1$.pipe(
// this returns a function used in the reducer.
map((data) => (prevData) => 'modifiedData',
scan((data, reducer) => reducer(betsMap), {})
)
obs2
const obs2$ = obs1$.pipe(
switchMap(data =>
someChange$.pipe(map(reducer => reducer(data)))
)
)
where someChange$ is a BehaviorSubject applying a change using another reducer function.
This works fine for async message #1 that makes some change.
But when message #2 comes in and a different change is needed, the first change is lost.
the changes that should be in "prevData" in obs1$ is always undefined because it happens before the message is applied.
How can I take the output from obs2$ and apply asynchronous updates to it that remembers what all of the past updates was? (in a way where I can clear all changes if needed)
So if i got the question right, there are two problems that this question tackles:
First: How to cache the last 2 emitted values from stream.
scan definitely is the right way, if this cache logic is needed in more than one place/file, I would go for a custom pipe operator, like the following one
function cachePipe() {
return sourceObservable =>
sourceObservable.pipe(
scan((acc, cur) => {
return acc.length === 2 ? [...acc.slice(1), cur] : [...acc, cur];
}, [])
);
}
cachePipe will always return the latest 2 values passed trough the stream.
...
.pipe(
cachePipe()
)
Second: How to access data from multiple streams at the same time, upon stream event
Here rxjs's combineLatest creation operator might do the trick for you,
combineLatest(API$, async1$ ,async2$,async3$)
.pipe(
// here I have access to an array of the last emitted value of all streams
// and the data can be passed to `Obs2` in your case
)
In the pipe I can chain whatever number of observables, which resolves the second problem.
Note:
combineLatest needs for all streams, inside of it, to emit once, before the operator strats to emit their combined value, one workaround is to use startWith operator with your input streams, another way to do it is by passing the data trough BehaviorSubject-s.
Here is a demo at CodeSandbox , that uses the cachePipe() and startWith strategy to combine the source (Obs1) with the async observables that will change the data.
I'm trying to understand how flatMap works. I understand that it is a way for handling Observable< Observable < T>>.
Anyhow, I was testing the behaviour of it and got stuck with this:
let plusDom = document.querySelector('#plus');
let minusDom = document.querySelector('#minus');
let minusSource = Rx
.Observable
.fromEvent(minusDom, 'click');
let plusSource = Rx.Observable
.fromEvent(plusDom, 'click');
plusSource
.flatMap(function(c){
console.log('Flatmap called');
return minusSource;
})
.subscribe(function(e){
console.log('done');
})
Here it is the jsbin: https://jsbin.com/sefoyowuqe/edit?html,js,console,output
I don't understand this behaviour:
3 clicks on plusDom prints:
Flatmap called
Flatmap called
Flatmap called
1 click on minusDom prints:
done
done
done
Why when clicking the minusDom it replays the events as many times as we have click the plusDom?
flatMap basically places the returned stream flat into the original stream. What you are probably looking for is switchMap, which will switch to that returned stream and switch to a new stream when the original source emits data by discarding the old one.
When in doubt, switchMap is usually the safest alternative to use.
See the marble-diagrams for comparison:
Flatmap doesn't remove previously "flattened" streams and :
Switchmap removes previously "switched" streams.