What is the best way to merge the results of several runs of an observable function

When working with Firestore, I need to retrieve multiple documents, each with a unique sourceAddressValue. This means for a list of N strings, I may need to fetch N documents.

I attempted the following approach:

getLocationAddresses(addresses: string[]) {
  const chunkSize = 10;
  let addressesChunks = [];
  if (addresses.length < chunkSize) {
      addressesChunks.push(addresses);
  } else {
    addressesChunks = [...Array(Math.ceil(addresses.length / chunkSize))].map(_ => addresses.splice(0,chunkSize));
  }
  console.log(addressesChunks);
  return of(...addressesChunks).pipe(
    mergeMap<string[], any>((x) => this.db.collection('locations', ref => 
    ref.where('sourceLocation', 'array-contains', x)).valueChanges()),
    toArray() // when this is removed, the code inside getOrders is triggered multiple times
);
  }

public getOrders() {
        this.getJSON().subscribe(data => {
            this.orders = data.orders;
            const addresses = this.orders.map(item => `${item.address}, ${item.postalCode}`);
            this.dbService.getLocationAddresses(addresses).subscribe(data => {
                console.log('data retrieved');
                console.log(data);
            });
            this.ordersRefreshed.next(this.orders);
        });
    }

However, it appears that the execution of the code is incomplete. Interestingly, when I remove the toArray() from getLocationAddresses, the subscribed function is triggered multiple times, once for each chunk.

Is there a way to group multiple completions of an observable function so that the observer is only fired once?

Answer №1

Before delving into the observed behavior, let's dissect what you're experiencing:

Upon removing toArray() from within getLocationAddresses, the subscribed function triggers multiple times, each time for a separate chunk.

The code enclosed in the subscribe block executes every time there's an emission received. By using mergeMap, you're essentially creating an observable with multiple "inner observables". Whenever any of these inner observables emit, the parent observable created by mergeMap will emit as well.

Hence, if you pass a total of n emissions to mergeMap, you should anticipate at least n emissions (note that these inner observables might emit more than once).

[when 'toArray' is included] it seems like the execution remains incomplete.

Using toArray() halts any emissions until the source observable completes; only then does it emit an array containing all the received emissions. In this context, the source represents the observable generated by

mergeMap</code, which consists of several <code>.valueChanges()
observables.

Nonetheless, observables stemming from firestore's .valueChanges() method will emit whenever any document in the returned collection undergoes changes but won't reach completion. Due to their enduring nature, toArray() won't yield anything.

This StackBlitz example elucidates the issue.

Possible Solutions

The resolution hinges on your desired outcome. Are you looking to execute each query once and retrieve the results (one-time action) OR maintain a dynamic stream that emits the most recent representation of your query?

Single Execution

To enforce an observable completing after receiving a sole emission, hence allowing toArray() to conclude as well, you can utilize take(1) (Example - 1A):

return of(...addressesChunks).pipe(
  mergeMap(x => 
    this.db.collection('locations', ref => 
      ref.where('sourceLocation', 'array-contains', x)
    ).valueChanges().pipe(take(1))  
  ),
  toArray()
);

Rather than employing of/mergeMap/toArray, an alternative approach involves using forkJoin (Example 1B):

return forkJoin(
  addressesChunks.map(
    x => this.db.collection('locations', ref => 
      ref.where('sourceLocation', 'array-contains', x)
    ).valueChanges().pipe(take(1))
  )
);

Dynamic Observable

To create an observable from numerous sources using combineLatest, emitting whenever any of these sources do so:

return combineLatest(
  addressesChunks.map(
    x => this.db.collection('locations', ref => 
      ref.where('sourceLocation', 'array-contains', x)
    ).valueChanges()
  )
);

Admittedly, this closely mirrors what firestore's .valueChages() inherently accomplishes. Despite the query segmentation, I'm intrigued about your rationale behind this.

Seems like you're issuing multiple queries solely to reunite the outcomes afterward.

You could potentially input all your addresses into a single call:

ref.where('sourceLocation', 'array-contains', addresses)

and obtain results promptly. Did you encounter any performance drawbacks following this approach?

Answer №2

When utilizing combineLatest, the getLocationAddresses() method produces combined results:

getLocationAddresses(addresses: string[]) {
      const chunkSize = 10;
      let addressesChunks: string[][] = [];
      if (addresses.length < chunkSize) {
          addressesChunks.push(addresses);
      } else {
        addressesChunks = [...Array(Math.ceil(addresses.length / chunkSize))].map(_ => addresses.splice(0,chunkSize));
      }
      console.log(addressesChunks);
      const observables = addressesChunks.map(addresses => this.db.collection('locations', ref => 
      ref.where('sourceLocation', 'in', addresses)).valueChanges());
      return combineLatest(observables)
      .pipe(map(arr => arr.reduce((acc, cur) => acc.concat(cur) ) ),);
  }

Answer №3

It is important to understand that valueChanges() returns an Observable that will alert you whenever the document it is linked to changes. This means that the Observable does not complete, resulting in streams created by mergeMap not completing either. As a result, toArray does not execute because it needs the source stream to complete.

On the other hand, combineLatest triggers when any of the observables provided as parameters have fired at least once and then one of them fires again.

If your goal is simply to fetch documents without being notified of changes, you can add a take(1) operator within each mergeMap.

The updated code would resemble this:

getLocationAddresses(addresses: string[]) {
  const chunkSize = 10;
  let addressesChunks = [];
  if (addresses.length < chunkSize) {
      addressesChunks.push(addresses);
  } else {
    addressesChunks = [...Array(Math.ceil(addresses.length / chunkSize))].map(_ => addresses.splice(0,chunkSize));
  }
  console.log(addressesChunks);
  return of(...addressesChunks).pipe(
    mergeMap<string[], any>((x) => this.db.collection('locations', ref => 
      ref.where('sourceLocation', 'array-contains', x)).valueChanges().pipe(
         take(1)
      )
    ),
    toArray() // removing this causes getOrders to trigger multiple times
);
}

take(1) generates the initial notification and then completes the Observable. Since the upstream observables are expected to eventually complete, toArray can be triggered.

I may not be well-versed in the Firebase rxJs library and there might be functions that encompass the behavior of take(1) described above. Nonetheless, I hope I conveyed the fundamental concept accurately.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

A guide on transferring files to a PHP server using HttpClient and FormData within an Ionic framework, along with instructions on receiving files in PHP

I have an input file in mypage.html on Ionic and I want to send this file to PHP for uploading it onto the server. In mypage.page.html, I have created an input field for the file name and another input field for the file to be uploaded. <ion-header> ...

typescript What is the best approach to searching within a nested array?

I am struggling to extract a specific value from a nested array within an array. Here is an example structure of my array: [ { ConcessionId: 1, ConcessionName: "Coyotes", KnownAs: [ { TeamId: 1, ...

Opt for Readme display over index.html

I'm just starting out and I have a question about Github: Is there a way to build a page using the index.html file within a folder instead of the readme file? I've successfully built many pages where the index file is not located in a folder. Ho ...

Angular14 offers a unique highcharts speedometer with multiple dials in the gauge chart for a visually stunning

Looking to create a unique speedometer design with an inner dial and an additional triangular indicator using Highcharts gauge within the Angular14 framework. Is it possible to include an extra indicator with Highcharts gauge in Angular14? Click on the l ...

Typescript interface created specifically for React Higher Order Component Props

Consider the React HOC provided below which adds sorting state to a component: import React, {Component, ComponentClass, ComponentType} from 'react' interface WithSortState { sortOrder: string } interface WithSortInjectedProps { sortO ...

SystemJS TypeScript Project

I am embarking on a journey to initiate a new TypeScript project. My aim is to keep it simple and lightweight without unnecessary complexities, while ensuring the following: - Utilize npm - Implement TypeScript - Include import statements like: import ...

Launching a MEAN stack application on Heroku

My current challenge involves deploying an application I have developed using the MEAN stack on Heroku. The issue seems to be related to the structure of my application. All server-side code is contained in a directory named 'server', which inclu ...

Unable to view loggly-winston debug logs on the user interface

I am having an issue where I cannot see any logs when calling winston.debug. It seems like the log level allowed to be seen needs to be changed. For more information, refer to the winston documentation or the Loggly node.js documentation. To start, instal ...

When using Angular msal_angular in conjunction with an ASP.NET Core Web API, an error may occur indicating an invalid token

I developed my Angular application using the guide provided in this example: https://github.com/microsoftgraph/msgraph-training-angularspa Successfully, I managed to log in and authenticate with MS Graph from within the Angular app. However, I am facing ...

Generate a fresh array by filtering objects based on their unique IDs using Angular/Typescript

Hey there, I am receiving responses from 2 different API calls. Initially, I make a call to the first API and get the following response: The first response retrieved from the initial API call is as follows: dataName = [ { "id": "1", ...

Issue with Angular 7 cli failing to recognize a custom TypeScript file

While working on an Angular 7 component, I encountered an issue when trying to read a custom file. The problem arises when the server restarts, even though there are no errors in the component's TypeScript file. ERROR: app/zontify-components/zonti ...

Guide to creating a Unit Test for an Angular Component with a TemplateRef as an Input

Looking to create unit tests for an Angular component that can toggle the visibility of contents passed as input. These inputs are expected to be defined as TemplateRef. my-component.component.ts @Component({ selector: "my-component", templateUrl ...

The issue with the tutorial is regarding the addHero function and determining the source of the new id

Whenever I need to introduce a new superhero character, I will utilize the add(string) function found in heroes/heroes.component.ts add(name: string): void { name = name.trim(); if (!name) { return; } this.heroService.addHero({ name } as H ...

Having T extend Record<string, any>, the keyof T does not return 'string' as a type

My goal is to achieve the following: type UserDataProps<FieldName extends keyof DataShape, DataShape extends Record<string, any>> = { id: string; value: DataShape[FieldName]; } const userDataBuilder = <FieldName extends keyof DataShape, ...

Incorporating an Angular Application into an Established MVC Project

I am working on an MVC project within an Area of a larger solution. My goal is to incorporate an Angular App into this area and integrate it with my MVC project. The catch is that this is not a .Net Core Mvc project. How can I configure my project to utili ...

What is the best way to calculate the difference between two dates using Angular?

How can I subtract two variables of type Date in Angular? Is there a built-in method for this, or do I need to create my own? This is what I have attempted: test: Date = new Date(); var1: Date = new Date('20-08-2018'); var2: Date = new Da ...

Methods for adjusting data based on the current user's login

I'm currently working on integrating a user login feature using Angular 6 for a stock management system. The user credentials are saved in the database, and I have successfully retrieved them into a component (login) for validation. After a successful ...

Angular fails to include the values of request headers in its requests

Using Django REST framework for the backend, I am attempting to authenticate requests in Angular by including a token in the request headers. However, Angular does not seem to be sending any header values. Despite trying various methods to add headers to ...

Having trouble resolving all parameters for the HomePage in Angular 2 and Ionic

What could be the issue in my code? I am attempting to utilize cordova-camera-plugins and Ionic native with Ionic 2, but upon running ionic serve, I encounter this Runtime Error: "Can't resolve all parameters for HomePage: ([object Object], [object Ob ...

Angular component unable to access function within service

I'm a newcomer to Angular and I'm encountering an issue with my service. In my service, I have a function to post a survey and a component. The problem lies in my component not being able to see the service. Additionally, when using UserIdleModul ...