"Efficiently Distributing HTTP Requests Among Simultaneous Observers using RxJS

Currently, I am working on a feature in my Angular application that requires handling multiple concurrent fetches for the same data with only one HTTP request being made. This request should be shared among all the subscribers who are requesting the data simultaneously. The initial subscriber should trigger the request immediately, and while that request is still pending, any new subscribers should also subscribe to the same ongoing request. Unlike typical caching mechanisms, once a request is completed, subsequent fetches will trigger a fresh HTTP request for updated data, bypassing the cached content. The caching process is managed separately within the application.

The current implementation I have is as follows (this is a simplified version):

class DataLoader<T> {

    private readonly requestInitiator = new Subject<void>();
    private readonly requests$: Observable<T>;

    constructor(requestCreator: () => Observable<T>) {
        this.requests$ = this.requestInitiator
            .pipe(
                mergeMap(requestCreator, 1), // concurrency set to 1
                share(),
            );
    }

    load(): Observable<T> {
        return new Observable<T>(observer => {
            this.requests$.subscribe(observer);
            this.requestInitiator.next();
        });
    }
}

This loader can be incorporated into an Angular singleton service like so:

@Injectable({ providedIn: 'root' })
export class SomeService {

    private readonly loader = new DataLoader<Something>(
        () => this.getHttp().get('/api/something'),
    );

    getSomething(): Observable<Something> {
        if (this.cache.isSomethingCached()) {
            return this.cache.getSomething();
        }

        return this.loader.load()
            .pipe(/* Implement caching if necessary */);
    }
}

When multiple components invoke SomeService.getSomething() concurrently, they will all share the same HTTP request seamlessly. While this setup seems to be functioning correctly, I am puzzled by the inner workings of it. Specifically, I am unsure why mergeMap() behaves as it does in this context. Initially, I tried using exhaustMap(), which did work most of the time but failed under certain conditions, causing some observers to become stuck without emitting any values or completing. It seems that in this scenario, mergeMap() with a concurrency setting of 1 behaves more like concatMap(), triggering just one HTTP request for each new observer. However, new requests are only sent if there isn't already an outstanding request. This behavior aligns perfectly with my requirements.

It appears that the addition of share() somehow alters the observable's behavior in a way that eludes me. Can someone shed light on why mergeMap() works with a concurrency value of 1 in this situation, while exhaustMap() does not?

Answer №1

It seems like the issue at hand involves multiple Angular components making requests to a remote server to retrieve data. The request should not hit the server if another call is already in progress, but any subsequent calls should still be able to receive the result from the ongoing call once it completes.

Once the first call is completed, subsequent calls should go through to the server as usual.

To address this problem, I would recommend the following approach:

  1. The remote service in Angular should have a method for executing the remote call and an Observable for emitting the results once received.
  2. Add a private boolean flag in the remote service that turns on when a call is made and off when the response is received.

A sample code snippet for the service could look something like this:

class DataService {
  private resultSubject = new Subject<any>();
  public resultObservable = this.resultSubject.asObservable();

  private callInProgress = false;

  fetchSomething(input: any) {
    if (this.callInProgress) {
      return;
    }
    this.callInProgress = true;
    makeRemoteCall(input).pipe(
      tap(() => this.callInProgress = false)
    ).subscribe(response => this.resultSubject.next(response));
  }
}

You can check out this StackBlitz example to see how this scenario is simulated.

Regarding your question about the "share" operator, it essentially introduces a Subject into the transformation pipeline to notify values received from upstream, similar to the approach suggested above for the data service.

Also, note that using mergeMap() with a concurrency of 1 is equivalent to concatMap(). It's interesting to see that mergeMap works while exhaustMap sometimes doesn't provide the expected outcome, which may seem counterintuitive.

Answer №2

Explaining the concept of the share operator:

Observable can be categorized into two types: hot and cold.

Ahot observable executes its actions regardless of whether there are subscribers or not.

On the other hand, a cold observable only carries out its actions when it has at least one subscriber, making it a lazy observable.

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

When using TypeScript, it is important to ensure that the type of the Get and Set accessors for properties returning a

Why is it necessary for TypeScript to require Get/Set accessors to have the same type? For example, if we want a property that returns a promise. module App { export interface MyInterface { foo: ng.IPromise<IStuff>; } export int ...

Exploring the Usage of Jasmine Testing for Subscribing to Observable Service in Angular's OnInit

Currently, I am facing challenges testing a component that contains a subscription within the ngOnInit method. While everything runs smoothly in the actual application environment, testing fails because the subscription object is not accessible. I have att ...

Is it possible to pass parameters from a base class's constructor to a child class?

I'm facing an issue with my base (generic) classes where the properties are initialized in the constructor. Whenever I try to extend these classes to create more specific ones, I find myself repeating the same parameters from the base class's con ...

Tips for resolving Typescript type error when overriding MuiContainer classes

My application is divided into two main files: (https://codesandbox.io/s/react-ts-muicontainer-override-yywh2) //index.tsx import * as React from "react"; import { render } from "react-dom"; import { MuiThemeProvider } from "@material-ui/core/styles"; imp ...

Retrieving the property of a union type comprising a void type and an unnamed type

Currently, I am working on a project involving GraphQL. In my code, I have encountered a GraphQLError object with a property named extensions. The type of this property is either void or { [key: string]: any; }. Whenever I try to access any property within ...

Running the Express service and Angular 6 app concurrently

Currently, I am in the process of developing a CRUD application using Angular6 with MSSQL. I have managed to retrieve data from my local database and set up the necessary routes, but I am encountering difficulties when it comes to displaying the data on th ...

How to convert form fields into JSON format using Angular 2

Currently, I am in the process of learning angular2 and have encountered a roadblock. I have created a form where the values are populated through JSON. The form consists of both pre-filled fields and text input fields where users can enter data and select ...

Exploring the process of associating a string with a specific enum value in TypeScript

One scenario is if you receive a string value from the server and have an enum type with string values defined. In TypeScript, how can you convert the string value to the enum type? export enum ToolType { ORA= 'orange', ST= 'stone' , ...

Potentially null object in react typescript

In my React application with TypeScript, I have completed the implementation of a chart but encountered an error in the following line: backgroundColor: gradientFill ? gradientFill : chartRef.current.data.datasets[0].backgroundColor, T ...

Develop a binary file in Angular

My Angular application requires retrieving file contents from a REST API and generating a file on the client side. Due to limitations in writing files directly on the client, I found a workaround solution using this question. The workaround involves crea ...

Tips for sending an array of any type to a Lookup function

I'm currently utilizing ngl-lookup from the ngl-lightning library and I'm attempting to pass an array of type any[] instead of String[]. Here's the code snippet I have: <ngl-lookup [lookup]="lookupManagerUsers" [icon]="true" [image]="&a ...

How can I achieve this using JavaScript?

I am attempting to create a TypeScript script that will produce the following JavaScript output. This script is intended for a NodeJS server that operates with controllers imported during initialization. (Desired JavaScript output) How can I achieve this? ...

I'm encountering an error when trying to use makeStyles

Something seems off with MUI. I was working on my project yesterday and makeStyles was functioning properly, but now it's suddenly stopped working. I'm encountering an error when calling it here: https://i.sstatic.net/tBf1I.png I suspect the iss ...

Tips for incorporating ngIf within a td element

My dilemma is with a table I have that displays data from a database. I need to be able to edit the data based on certain qualifications, so I want to include two buttons - one for deleting and one for editing. These buttons should only be enabled if the r ...

Ineffectiveness of ngFor in displaying content within a component in Ionic 5

Feeling a bit overwhelmed here, I've successfully created a new component and it's working perfectly. Now I want to use this component on two different pages. To make things easier, I decided to create a component module (/components/all-compone ...

Manage the recently implemented features with TypeScript in IntelliJ

When using IntelliJ with TypeScript and referencing a new function in an .html file, IntelliJ has the option to automatically add this function to the corresponding .component.ts file. For example: <div *ngIf="ifConditionIsTrue()"> Intell ...

Updating data on change when using a TemplateRef from the parent component in a child component in Angular: A comprehensive guide

I am facing an issue with a child component that renders a template from the parent using `createEmbeddedView`. exports class ParentComponent { public someProp: string; ngOnInit() { this.someHttpFunc(); } public someHttpFunc() { ...

Ensure the object is not null with this Object type guard

Is there a way to create a type guard for an object directly in TypeScript? I've written a function to check any input: export function isObject(input: any) :input is Record<string,any> { return (input !== null) && (typeof input == ...

Obtain the selected value of 'id' from an autocompletion feature in Angular Material

How can I configure mat-autocomplete to retrieve the ID of the selected option? <mat-form-field> <input type="text" matInput [formControl]="autocompleteControl" [matAutocomplete]="auto"> ...

Problems encountered while starting npm in Angular 13

Versions -> PS C:\Users\user> npm --version 8.8.0 PS C:\Users\user> node --version v16.15.0 Executing the following command-> npx -p @angular/cli ng new JokeFrontB After that, I run Serve -> npm start and encountered t ...