How do I iterate over functions that return rxjs observables - rxjs

I want to iterate over a series of asynchronous functions and end the iterating when a false is returned.
I'm new to rxjs and can't get the use-case below to work. I feel like I'm not understanding something fundamental. Can someone please point it out to me?
function validateA(): Observable<any> {
// do stuff.
return of({ id: "A", result: true }); // hardcoding result for now
}
function validateB(): Observable<any> {
// do stuff
return of({ id: "B", result: true }); // hardcoding result for now
}
function validateC(): Observable<any> {
// do stuff
return of({ id: "C", result: false });// hardcoding result for now
}
from([validateA, validateB, validateC])
.pipe(
map(data => data()),
takeWhile(data => !!data.result)
)
.subscribe(data => console.log(`${data.id} passed!`));
https://stackblitz.com/edit/typescript-ub9c5r?file=index.ts&devtoolsheight=100

I would say that the core of your logic is right. What is missing is some rxJs pecularity.
The solutions could be something like this. Explanation of the nuances are in the comments.
// start from an array of functions and turn it into a stream using RxJs from function
from([validateA, validateB, validateC])
.pipe(
// now execute each function sequentially, one after the other, via concatMap
// operator. This operator calls each function and each function returns an Observable
// concatMap ensures that the functions are called sequentially and also that the returned Observable (because each function returns an Observable)
// is "flattened" in the result stream. In other words, you execute each function one at the time
// and return the value emitted by the Observable returned by that function
// until that Observable completes. Considering that you use the "of" function to
// create the Observable which is returned by each function, such Observable emits just one value and then completes.
concatMap(func => func()),
// now you have a stream of values notified by the Observables returned by the functions
// and you terminate as soon as a flase is received
takeWhile(data => !!data.result)
)
.subscribe(data => console.log(`${data.id} passed!`));

The following seems to do the trick and calls functions lazily:
https://stackblitz.com/edit/typescript-9ystxv?file=index.ts
import { from, Observable, of } from "rxjs";
import { concatAll, find, map } from "rxjs/operators";
function validateA() {
console.log('validateA');
return of({ id: "A", result: false });
}
function validateB() {
console.log('validateB');
return of({ id: "B", result: true });
}
function validateC() {
console.log('validateC');
return of({ id: "C", result: false });
}
from([validateA, validateB, validateC])
.pipe(
map(validate => validate()),
concatAll(),
find(data => data.result)
)
.subscribe(data => console.log(`${data.id} passed!`));

Related

How can i execute asynchronous code when an RxJS observable complete?

I would like to execute code when the observable complete. In my code, i execute this:
compact(): Observable<FileManifest> {
return this.loadIndex().pipe(
mergeMap((index) => index.walk()),
map((entry) => entry.manifest),
notUndefined(),
writeAllMessages(this.newPath, ProtoFileManifest),
finalize(async () => {
await Promise.all([
promises.rm(this.journalPath, { force: true }),
promises.rm(this.manifestPath, { force: true }),
]);
await promises.rename(this.newPath, this.manifestPath);
}),
);
}
The problem is that the finalize method is made for synchronous code. When i execute asynchronous code like above, the code will be executed independently from the subscribe.
I would like this will be execute when disposing resource of the observable but i want that when i subscribe, i always receive the event.
How can i put asynchronous code in the finalize method ?
Thanks
Ulrich
One way to do it is to create three observables instead of trying to do it all
in one. Each will make up a link in the sequential async chain you want to
make.
In order for the side effects in the promise-based observables to be lazy, we use defer.
Note that the defer callback's return value can be an observable, or an
"ObservableInput", which is what RxJS calls values it knows how to turn
into observables. This value can be (among other things) a promise.
({
compact(): Observable<FileManifest> {
const writeToTempManifest$ = this.loadIndex().pipe(
mergeMap((index) => index.walk()),
map((entry) => entry.manifest),
notUndefined(),
writeAllMessages(this.newPath, ProtoFileManifest)
);
const removeOldManifest$ = defer(() =>
Promise.all([
promises.rm(this.journalPath, { force: true }),
promises.rm(this.manifestPath, { force: true }),
])
);
const renameNewManifest$ = defer(() =>
promises.rename(this.newPath, this.manifestPath)
);
return from([
writeToTempManifest$,
removeOldManifest$,
renameNewManifest$,
]).pipe(concatAll());
},
});
Note that each of these observables potentially emits something (though I'm not familiar with the API). The first emits whatever the writeAllMessages operator does, while the second and third emit the resolved values of their respective promises. In the case of the second one, that's a two element array from the Promise.all.
If you want to suppress an observable's emitted values while still keeping it open until it completes, you can create an operator that does just that:
const silence = pipe(concatMapTo(EMPTY));

How can you execute and array of Observables API calls where each API calls waits for the previous?

What I have been playing with is to use combineLatest with concatAll() but they are still being called simultaneously. I could just loop and call each but I am always wondering if there is a better way within the RXJS workflow.
combineLatest(arrayOfApiObservables).pipe(concatAll()).subscribe();
The problem here is that you use the combineLatest operator, which will emit value only after all observables had emitted (e.g. it is calling everything simultaneously).
After that the concatAll can't affect the arrayOfApiObservables because they have alredy been called.
The right aproach is to create a higher-order observable (observable that emits observables), which can be achived with the help of the operator from and after that you can concatAll them to achive the desired result.
concatAll definition as seen in the docs: Converts a higher-order Observable into a first-order Observable by concatenating the inner Observables in order..
let {
interval,
from
} = rxjs
let {
take,
concatAll,
mapTo
} = rxjs.operators
let ref = document.querySelector('#container')
const obs1$ = interval(1000).pipe(take(1), mapTo('obs1'));
const obs2$ = interval(500).pipe(take(1), mapTo('obs2'));
const obs3$ = interval(2000).pipe(take(1), mapTo('obs3'));
let allObservables$ = from([obs1$, obs2$, obs3$])
allObservables$.pipe(
concatAll()
).subscribe((x) => {
console.log(x)
container.innerHTML += `<div>${x}</div>`
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.5/rxjs.umd.js"></script>
<div id="container"></div>
If you want them to be executed in a sequential manner, you could use concatMap() inside pipe().
Something like this:
of(1)
.pipe(
concatMap(result => {
console.log(result);
return of(2);
}),
concatMap(result => {
console.log(result);
return of(3);
}),
concatMap(result => {
console.log(result);
return of(4);
}),
concatMap(result => {
console.log(result);
return of(5);
}),
concatMap(result => {
console.log(result);
return of(6);
})
)
.subscribe(res => {
console.log("finish");
});
But, if you want to execute them all at once and then await for them until they are all completed, then just use forkJoin().

distinctUntilChanged in nested pipe with switchMap

I have an observable stream set up as below. I have an interval that is polling every two seconds. I then switchMap that to make two dependent API calls (mocked here with 'of's). After, I want to use distinctUntilChanged to make sure the final object is different. The only thing is that distinctUntilChanged doesn't fire.
I'm assuming it has SOMETHING to do with the fact that we are creating new streams and therefore never collects two objects to compare, but I don't fully understand.
interval(2000).pipe(
switchMap(() => loadData()),
)
.subscribe(res => console.log(res)); // { name: 'test' } is printed every two seconds
function loadData() {
return of('API call').pipe(
mergeMap(numb => of({ name: 'test' })),
distinctUntilChanged((prev, cur) => {
console.log('CompareFn'); // This will never fire.
return JSON.stringify(prev) === JSON.stringify(cur)})
);
}
Stackblitz: https://stackblitz.com/edit/rxjs-ko6k3c?devtoolsheight=60
In this case, I would like there to only be a single value ever printed from the next handler as distinctUntilChanged should stop all values after the first.
Would appreciate an explanation as to why this isn't working as I would expect it to.
the problem is that your distinctUntilChanged is operating on the inner observable, not the outter... you need to do this
interval(2000).pipe(
switchMap(_ => loadData()),
distinctUntilChanged((prev, cur) => {
console.log('CompareFn');
return JSON.stringify(prev) === JSON.stringify(cur);
})
)
.subscribe(res => console.log(res));
function loadData() {
return of('API call').pipe(
mergeMap(numb => of({ name: 'test' }))
);
}
in your prior set up, only one value ever reached distinctUntilChanged as the interval switched into a new observable via switch map.

rxjs - combining inner observables after filtering

I call backend that respond with:
[
"https://some-url.com/someData1.json",
"https://some-url.com/someData2.json"
]
Each JSON can have following schema:
{
"isValid": boolean,
"data": string
}
I want to get array with all data, that have isValid is set to true
backend.get(url)
.pipe(
mergeMap((urls: []) =>
urls.map((url: string) =>
backend.get(url)
.pipe(
filter(response => response.isValid),
map(response => response.data)
)
)
),
combineAll()
)
When both .json have "isValid" set to true, I get array with both data.
But when one of them has "isValid" set to false observable never completes.
I could use mergeAll instead of combineAll, but then I receive stream of single data not collection of all data.
Is there any better way to filter out observable?
As you said, the inner observable never emits, because filter does not forward the only value that is ever emitted by the backend.get observable. In that case, the operator subscribing on that observable - in your case combineAll - will also never receive any value and cannot ever emit itself.
What I would do is just move the filtering and mapping to combineAll by providing a project function, like that:
backend.get(url)
.pipe(
mergeMap((urls: string[]) =>
urls.map((url: string) => backend.get(url))
),
combineAll(responses =>
responses
.filter(response => response.isValid)
.map(response => response.data)
)
)
See if that works for you ;)
import { forkJoin, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
interface IRes {
isValid: boolean;
data: string;
}
interface IResValid {
isValid: true;
data: string;
}
function isValid(data: IRes): data is IResValid {
return data.isValid;
}
const res1$: Observable<IRes> = backend.get(url1);
const res2$: Observable<IRes> = backend.get(url2);
// When all observables complete, emit the last emitted value from each.
forkJoin([res1$, res2$])
.pipe(map((results: IRes[]) => results.filter(isValid)))
.subscribe((results: IResValid[]) => console.log(results));

Observable from Subject

I'm trying to create actions from updates from a RX Subject
It's working but I get the error below.
Here is my Epic
export function uploadSceneFile(action$, store) {
return action$.ofType(CREATE_SCENE_SUCCESS)
.mergeMap(({payload}) =>
UploadSceneWithFile(payload)
.subscribe(res => {
if (res.progress > 0)
store.dispatch(uploadSceneProgress(res))
else if(res.progress === -1){
store.dispatch(uploadSceneSuccess(res))
requestSceneProcessing(res).map(res => {
})
}
})
)
}
And here is the Subject
export function UploadSceneWithFile(scene){
const subject$ = new Subject()
const uploader = new S3Upload({
getSignedUrl: getSignedUrl,
uploadRequestHeaders: {'x-amz-acl': 'public-read'},
contentType: scene.file.type,
contentDisposition: 'auto',
s3path: 'assets/',
onError:()=>subject$.next('error'),
onProgress: (val)=> subject$.next({...scene,progress:val}),
onFinishS3Put: ()=>subject$.next({...scene,progress:-1}),
})
uploader.uploadFile(scene.file)
return subject$
}
I read from a previous post that I'm supposed to be using .map, not .subscribe but nothing happens if I don't subscribe (the upload doesn't happen)
What's the best way of doing this?
subscribeToResult.js:74 Uncaught TypeError: You provided an invalid object where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
at Object.subscribeToResult (subscribeToResult.js:74)
at MergeMapSubscriber../node_modules/rxjs/operators/mergeMap.js.MergeMapSubscriber._innerSub (mergeMap.js:132)
at MergeMapSubscriber../node_modules/rxjs/operators/mergeMap.js.MergeMapSubscriber._tryNext (mergeMap.js:129)
at MergeMapSubscriber../node_modules/rxjs/operators/mergeMap.js.MergeMapSubscriber._next (mergeMap.js:112)
at MergeMapSubscriber../node_modules/rxjs/Subscriber.js.Subscriber.next (Subscriber.js:89)
at FilterSubscriber../node_modules/rxjs/operators/filter.js.FilterSubscriber._next (filter.js:89)
at FilterSubscriber../node_modules/rxjs/Subscriber.js.Subscriber.next (Subscriber.js:89)
at Subject../node_modules/rxjs/Subject.js.Subject.next (Subject.js:55)
at createEpicMiddleware.js:60
at createEpicMiddleware.js:59
at SafeSubscriber.dispatch [as _next] (applyMiddleware.js:35)
at
The problem is that you subscribe inside mergeMap and return a Subscription which is invalid. The callback needs to return only Observable, Promise, Array, or Iterable.
I'm not sure what exactly you need to do but if you need to perform some side-effects you can use do() operator instead of subscribing.
export function uploadSceneFile(action$, store) {
return action$.ofType(CREATE_SCENE_SUCCESS)
.mergeMap(({ payload }) => UploadSceneWithFile(payload)
.do(res => {
...
})
)
}
Or it looks like you could put do after mergeMap as well:
export function uploadSceneFile(action$, store) {
return action$.ofType(CREATE_SCENE_SUCCESS)
.mergeMap(({ payload }) => UploadSceneWithFile(payload))
.do(res => {
...
});
}

Resources