rxjs switchMap cache the obsolete result and do not create new stream - rxjs

const s1$ = of(Math.random())
const s2$ = ajax.getJSON(`https://api.github.com/users?per_page=5`)
const s3$ = from(fetch(`https://api.github.com/users?per_page=5`))
const click$ = fromEvent(document, 'click')
click$.pipe(
switchMap(() => s1$)
).subscribe(e => {
console.log(e)
})
I was confused by the code above and can not reason about them properly.
In the first case(s1$), the same result is received every time, it LOOKs fine to me even though I can not understand why switchMap do not start a new stream each time. OK, it is fine
The really wired thing happen when you run s2$ and s3$, the looks equivalent, right? WRONG!!! the behaviours are completely different if you try them out!
The result of s3$ is cached somehow, i.e. if you open the network panel, you will see the http request was send only ONCE. In comparison, the http request is sent each time for s2$
My problem is that I can not use something like ajax from rx directly because the http request is hidden a third-party library, The solution I can come up with is to use inline stream, i.e. create new stream every time
click$.pipe(
switchMap(() => from(fetch(`https://api.github.com/users?per_page=5`)))
).subscribe(e => {
console.log(e)
})
So, how exactly I can explain such behaviour and what is the correct to handle this situation?

One problem is that you actually execute Math.random and fetch while setting up your test case.
// calling Math.random() => using the return value
const s1$ = of(Math.random())
// calling fetch => using the return value (a promise)
const s3$ = from(fetch(`https://api.github.com/users?per_page=5`))
Another is that fetch returns a promise, which resolves only once. from(<promise>) then does not need to re-execute the ajax call, it will simply emit the resolved value.
Whereas ajax.getJSON returns a stream which re-executes every time.
If you wrap the test-streams with defer you get more intuitive behavior.
const { of, defer, fromEvent } = rxjs;
const { ajax } = rxjs.ajax;
const { switchMap } = rxjs.operators;
// defer Math.random()
const s1$ = defer(() => of(Math.random()));
// no defer needed here (already a stream)
const s2$ = ajax.getJSON('https://api.github.com/users?per_page=5');
// defer `fetch`, but `from` is not needed, as a promise is sufficient
const s3$ = defer(() => fetch('https://api.github.com/users?per_page=5'));
const t1$ = fromEvent(document.getElementById('s1'), 'click').pipe(switchMap(() => s1$));
const t2$ = fromEvent(document.getElementById('s2'), 'click').pipe(switchMap(() => s2$));
const t3$ = fromEvent(document.getElementById('s3'), 'click').pipe(switchMap(() => s3$));
t1$.subscribe(console.log);
t2$.subscribe(console.log);
t3$.subscribe(console.log);
<script src="https://unpkg.com/#reactivex/rxjs#6/dist/global/rxjs.umd.js"></script>
<button id="s1">test random</button>
<button id="s2">test ajax</button>
<button id="s3">test fetch</button>

Related

Process.all(array.map(... doesn't work in parallel with page.goto(

I am using the pupperteer library for my bot and I would like to perform some operations in parallel.
In many articles, it is advised to use this syntax :
await Promise.all(array.map(async data => //..some operations))
I've tested this on several operations and it works but when I embed the code below in my .map promise
await page.goto(..
It did not work during Operation Promise and it considers this to be a synchronous operation.
I would like to know why it reacts like this?
I believe your error comes from the fact that you're using the same page object.
The following should work:
const currentPage = browser.pages().then(allPages => allPages[0]);
const anotherPage = await browser.newPage();
const bothPages = [currentPage, anotherPage];
await Promise.all(
bothPages.map(page => page.goto("https://stackoverflow.com"))
);

Pipe Observable to Subject without making it hot unneccessarily

This question builds upon this one, where it is shown how to feed an Observable into a Subject. My question is similar, but I want to avoid making the Observable hot unless it is necessary, so that it's .pipe() doesn't run needlessly. For example:
const subject$ = new Subject();
const mouseMove$ = Observable.fromEvent(document, 'mousemove')
.pipe(map(it => superExpensiveComputation(it)));
mouseMove$.subscribe(n => subject$.next(n));
Because of the subscription, this will make mouseMove$ hot, and superExpensiveComputation will be run for every mouse move, whether someone is listening for it on subject$ or not.
How can I feed the result of mouseMove$ into subject$ without running superExpensiveComputation unneccessarily?
You can simply use tap instead of subscribe to pass emissions to your subject:
const mouseMove$ = fromEvent(document, 'mousemove').pipe(
map(it => superExpensiveComputation(it)),
tap(subject$)
);
Of course you still need to subscribe to mouseMove$ to make the data flow, but you don't need to have a subscription dedicated to passing the data to your subject.
However, you'll probably want to add share as not to repeat the expensive logic for multiple subscribers.
const mouseMove$ = fromEvent(document, 'mousemove').pipe(
map(it => superExpensiveComputation(it)),
share(),
tap(subject$)
);
But... then in that case, do you really need a subject at all? Unless you are going to be calling .next() from somewhere else, you probably don't.
Because of the subscription, this will make mouseMove$ hot
Wrong. The subscription doesn't change behavior of observable.
Observable.fromEvent(document, 'mousemove') is already hot.
Here's naive cold version of it.
The key takeaway, I have to resubscribe it everytime to get the latest data.
const { of } = rxjs;
const mouseMove$ = of((() => {
let payload = {clientX: 0};
document.addEventListener("mousemove", (ev) => payload.clientX = ev.clientX)
return payload;
})())
let subscriber = mouseMove$.subscribe(pr => console.log(pr));
const loop = () => {
if(subscriber) {
subscriber.unsubscribe();
}
subscriber = mouseMove$.subscribe(pr => console.log(pr));
setTimeout(loop, 1000);
};
setTimeout(loop, 1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.5/rxjs.umd.js"></script>
and superExpensiveComputation will be run for every mouse move,
whether someone is listening for it on subject$ or not
Then you can get rid of map(it => superExpensiveComputation(it)) and move that lamda to subscription so it still executes on every mouse move but after it has been subscribed..

RXJS How do I load an array of images while successfully handling errors?

I am looking to preload a bunch of images, and have discounted base64 and createObjectURL so i'd take a better option, but this is what I have.
Anyway this is what I am looking at, a function which does this. Loads an array of URLS as images.
const urls = ["lol.jpg"];
const images = urls.map((url) => {
const imageElement = document.createElement('img');
const imageComplete = fromEvent(imageElement, 'load');
imageElement.src = targetURL;
return imageComplete;
});
forkJoin(images)
But how do I correctly handle loading errors here? I have added a new fromEvent but now I have 2 events where I used to just have one, and further one of them is the special error case.
const urls = ["lol.jpg"];
const images = urls.map((url) => {
const imageElement = document.createElement('img');
const imageComplete = fromEvent(imageElement, 'load');
const imageError = fromEvent(imageElement, 'error');
imageElement.src = targetURL;
return imageComplete; // <--- not good enough now
});
forkJoin(images)
Is it correct to listen for an error here? Ultimately I need to know if any of these fail and consider them all failures but during my tests, a 404 doesn't catchError anywhere and this brings me to this question.
The answer, in part, depends on what fromEvent(imageElement, 'error') does here:
fromEvent(imageElement, 'error').subscribe({
next: val => console.log("next: ", val),
error: err => console.log("error:", err)
});
If you do this, and you receive an error, does the event trigger next or error? Either way, I assume you want to remove imageElement if it fails to load.
If it triggers error, then you can do this:
const imageComplete = fromEvent(imageElement, 'load');
const imageError = fromEvent(imageElement, 'error').pipe(
catchError(err => {
imageElement.remove();
return throwError(err);
})
);
imageElement.src = targetURL;
return merge(imageComplete, imageError);
If it triggers next, then you can switch imageError into a stream that errors out by throwing an error like this:
const imageComplete = fromEvent(imageElement, 'load');
const imageError = fromEvent(imageElement, 'error').pipe(
tap(x => {
imageElement.remove();
throw(new Error(x.message));
})
);
imageElement.src = targetURL;
return merge(imageComplete, imageError);
Now, forkJoin will fail with your error if any of its sources fail. If you don't want that, you need to handle the error before it reaches your forkJoin.
Retry failed images a few times instead
The trickiest bit here is that you need to create an image element as part of your stream so that when you re-try it will not just resubscribe to an event of an already failed element.
The other tricky bit is that you want to create your imageElement as part of subscribing to your stream rather than as part of its definition. That's what defer essentially accomplishes for you.
With that out of the way, not many changes, and you can re-try loading your image pretty simply. You can read up on retryWhen if you want to make the process a bit more sophisticated (Like delay a bit before retrying).
const images = urls.map(targetURL =>
defer(() => of(document.createElement('img'))).pipe(
mergeMap(imageElement => {
const imageComplete = fromEvent(imageElement, 'load');
const imageError = fromEvent(imageElement, 'error').pipe(
catchError(err => {
imageElement.remove();
return throwError(err);
})
);
imageElement.src = targetURL;
return merge(imageComplete, imageError);
}),
retry(5),
catchError(err => {
console.log(`Failed to load img (${targetURL}) 5 times`);
// Throw error if you want forkJoin to fail, emit anything else
// if you want only this image to fail and the rest to keep going
return of(new DummyImage());
})
)
);
forkJoin(images);
In this case, since we've dealt with errors before they hit the forkJoin, we can expect the array of images to contain a DummyImage anywhere that an image failed to load 5 times. You can make DummyImage anything, really. Have a low-res default img loaded locally, for example.
If you return throwError(err) instead, then the entire forkJoin will fail the moment any image fails to load 5 times (It'll still retry 5 times though) and you'll not get any images. That might be what you want?

Pausable buffer with RxJS

I'm trying to implement a togglable auto-save feature using RxJS streams. The goal is to:
While auto-save is enabled, send changes to the server as they come.
While auto-save is disabled, buffer the changes and send them to the server when auto-save is re-enabled.
Here is what I came across with:
autoSave$ = new BehaviorSubject(true);
change$ = new Subject();
change$.pipe(
bufferToggle(
autoSave$.pipe(filter(autoSave => autoSave === false)),
() => autoSave$.pipe(filter(autoSave => autoSave === true)),
),
concatMap(changes => changes),
concatMap(change => apiService.patch(change)),
).subscribe(
() => console.log('Change sent'),
(error) => console.error(error),
);
Thanks to bufferToggle, I'm able to buffer the changes while autoSave is off and send them when it's re-enabled.
Problem is that while autoSave is enabled, nothing passes through. I understand it's because bufferToggle ignores the flow coming while its opening observable doesn't emit.
I feel that I should have a condition there to bypass the bufferToggle while autoSave is enabled, but all my attempts miserably failed.
Any idea to achieve this?
We can buffer events in-between autosave on and off using bufferToggle(on, off), and open a filtering window between off and on using windowToggle(off, on). And then we merge those together:
const on$ = autoSave$.filter(v=>v);
const off$ = autoSave$.filter(v=>!v);
const output$ =
Observable.merge(
changes$
.bufferToggle(
off$,
()=>on$
)
changes$
.windowToggle(
on$,
()=>off$
)
)
.flatMap(x=>x) // < flattern buffer and window
Play with this example at https://thinkrx.io/gist/3d5161fc29b8b48194f54032fb6d2363
* Please, note that since buffer wraps values in Array — I've used another flatMap(v=>v) in the example to unwrap buffered values. You might want to disable this particular line to get arrays from buffers mixed with raw values.
Also, check my article "Pausable Observables in RxJS" to see more examples.
Hope this helps
Another solution.
Just one observable to play / pause
export type PauseableOptions = 'paused' | 'playing'
export function pauseableBuffered(pauser$: Observable<PauseableOptions>) {
return function _pauseableBuffer<T>(source$: Observable<T>): Observable<T> {
let initialValue = 'paused'
// if a value is already present (say a behaviour subject use that value as the initial value)
const sub = pauser$.subscribe(v => initialValue = v)
sub.unsubscribe()
const _pauser$ = pauser$.pipe(startWith(initialValue), distinctUntilChanged(), shareReplay(1))
const paused$ = _pauser$.pipe(filter((v) => v === 'paused'))
const playing$ = _pauser$.pipe(filter((v) => v === 'playing'))
const buffer$ = source$.pipe(bufferToggle(paused$, () => playing$))
const playingStream$ = source$
.pipe(
withLatestFrom(_pauser$),
filter(([_, state]) => state === 'playing'),
map(([v]) => v)
)
return merge(
buffer$.pipe(
mergeMap(v => v)
),
playingStream$
)
}
}
const stream$ = new Subject<number>()
const playPause$ = new BehaviorSubject<PauseableOptions>('playing')
const result: number[] = []
const sub = stream$.pipe(pauseableBuffered(playPause$))
.subscribe((v) => result.push(v))

Start stream when another Observable emits its first value

I have two observables, one from key press events and another from ajax requests.
I want the second stream to start when the first stream emits its first value.
var input$ = Rx.Observable.fromEvent( input, 'keydown')
.debounceTime(500)
var countries$ = Rx.Observable.of('https://restcountries.eu/rest/v1/all')
.flatMap(url => $.get( url ))
.retryWhen( errors => {
return errors.scan( (sum, err) => {
if( sum === 2 )
{
throw err;
}
else{
return sum + 1;
}
}, 0).delay(1000)
})
I am using rxjs 5, I want the countries$ observable to start when the input$ observable emits its first value. I tried using skipUntil or debounce passing the input$ Observable but... no luck. I see the ajax request happening in the network tab before I start typing anything. Any idea how I can achieve what I want?
Use switchMap. This will switch from the source stream to the new stream whenever the source Observable emits
var url = 'https://restcountries.eu/rest/v1/all';
var countries$ = input$.switchMap(input => $.get(url))
.retryWhen(...
Note that if a new input arrives before the http request is completed, the old request will be cancelled and a new one issued. So switchMap is perfect if the http request made depends on the specific input (as in a search query)
I used a static URL because that's what your OP uses, but you could easily alter the URL to include a query parameter built from the input.
If you wish to run the http request just once regardless of what the input was, and do not want to cancel the old request when a new input arrives, use take to only read from the source once:
var countries$ = input$.take(1).switchMap(input => $.get(url))
.retryWhen(...

Resources