Filter an observed array with a observable condition

Viewed 582

I have a request, which returns an array of objects. Each object includes an id, with which I send another request. Based on this result I want to filter the array. Simplified example:

function getAllObjects(): Observable<{ id: number }[]> {
  return of([
    { id: 1 },
    { id: 2 },
    { id: 3 },
    { id: 4 },
  ]);
}

function checkObject(obj): Observable<boolean> {
  return of(obj.id % 2 === 0);
}

getAllObjects().pipe(
  // TODO
).subscribe(console.log); // I only want to see objects here which passed the async check
4 Answers

Does that work for you?

getAllObjects().pipe(
  flatMap((ar) => ar),
  concatMap((obj) => combineLatest([of(obj), checkObject(obj)])),
  filter(([_, checkResult]) => checkResult),
  map(([obj]) => obj),
  toArray(),
).subscribe(console.log);

Edit, I see you already found a solution, mine isn't much simpler, and I thought you wanted a stream of objects rather than return them as an array. So I added toArray in my Edit.

A solution that may not be the simplest but it also works

getAllObjects()
    .pipe(
        switchMap(array =>
            combineLatest(array
                .map(obj =>
                    checkObject(obj)
                        .pipe(
                            distinctUntilChanged(),
                            map(boolean => boolean ? obj : null)
                        )
                )
            )
        ),
        map(array => array.filter(obj => obj))
    )
    .subscribe(console.log);

Taking into account possible changes in real time

function getAllObjects(): Observable<{ id: number }[]> {
    return timer(0, 10000)
        .pipe(
            map(() => [
                { id: 1 },
                { id: 2 },
                { id: 3 },
                { id: 4 },
            ])
        );
}

function checkObject(obj): Observable<boolean> {
    return timer(1000, 5000)
        .pipe(
            map(() => obj.id % 2 === 0)
        );
}
const objects$ = getAllObjects().pipe(concatAll());
const objectValidations$ = objects$.pipe(concatMap(checkObject));

zip(objects$, objectValidations$).pipe(
    filter(([, validated]) => validated),
    map(([obj,]) => obj),
    toArray()
).subscribe(console.log);

UPDATE: We can improve the above solution performance-wise by parallelizing the "checks":

getAllObjects().pipe(
  concatAll(),
  mergeMap(obj => 
    checkObject(obj).pipe(
      map(isValid => isValid? obj : undefined),
      filter(Boolean),
    )
  ),
  toArray()
)

That's better because if we assume the following implementation of checkObject (added a delay):

function checkObject(obj) {
  return of(obj.id % 2 === 0).pipe(delay(1000));
}

Then for n objects, the previous solution takes n seconds, as opposed to 1 second with the updated solution

import { map, filter } from 'rxjs/operators';


map(items => items.filter(item => item.id % 2 === 0)),
filter(items => items && items.length > 0)

First use the map function and filter the array like normal. Then to make sure you don't get null or empty arrays, use the filter function which won't call the subscription if the map is null or empty.

Related