Consolidating Angular 4 Observable HTTP requests into a single Observable to optimize caching

I am currently working on an Angular 4 application that serves as a dashboard for a system. Several different components within the application make calls to the same REST endpoint using identical TypeScript service classes. While this setup functions correctly, I am looking to prevent unnecessary duplicate requests from overwhelming the server by introducing a caching mechanism on the client side.

To address this, I have developed a caching solution in TypeScript which is utilized by my services. The services pass the HTTP call into a computeFunction:

@Injectable()
export class CacheService {

  private cacheMap = {};


  getAsObservable<V>(
                key: string,
                expirationThresholdSeconds: number,
                computeFunction: () => Observable<V>): Observable<V> {

    const cacheEntry = this.cacheMap[key];

    if (...) {
      return Observable.of<V>(cacheEntry.value);          
    } else {
      return computeFunction().map(returnValue => {

        const expirationTime = new Date().getTime() + (expirationThresholdSeconds * 1000);

        const newCacheEntry = ... // build cache entry with expiration set

        this.cacheMap[key] = newCacheEntry;

        return returnValue;
    });
  }

}

While this approach works as intended, I have noticed that consecutive calls made with the same key can lead to multiple server requests as the cache may not yet contain the return value at the time of verification.

Therefore, I believe it might be beneficial to create a custom cacheable wrapper "multiplexing" Observable that can be shared among multiple subscribers:

  1. Execute the computeFunction only once
  2. Cache the returned value
  3. Distribute the cached value to all subscribers and manage cleanup like regular HTTP Observables do

I would appreciate it greatly if someone could provide me with a sample implementation of this concept.

The primary challenge lies in ensuring that the Observable can handle scenarios where:

  • Subscriptions are established before the wrapped computeFunction returns (waiting until the subscription occurs)
  • Subscriptions are initiated after the wrapped computeFunction has already returned (providing the cached value)

Am I potentially overcomplicating this process? If there is a simpler approach that I should consider, I am eager to learn about it.

Answer №1

There's no need for complex logic here. Simply use the shareReplay(1) method to multicast the observable. Check out this example:

// Simulating an asynchronous call
// Adding a side effect logging to track when our API call is made
const api$ = Observable.of('Hello, World!').delay(1000)
    .do(() => console.log('API called!'));

const source$ = api$
     // Ensure that the observable doesn't complete so shareReplay() doesn't reconnect if ref count drops to zero
    .concat(Observable.never())
     // Let the magic of multicasting happen!
    .shareReplay(1);

You can now subscribe as much as you'd like:

// Two parallel subscriptions
const sub1 = source$.subscribe();
const sub2 = source$.subscribe();

// A new subscription when ref count > 0
sub1.unsubscribe();
const sub3 = source$.subscribe();

// A new subscription after ref count becomes 0
sub2.unsubscribe();
sub3.unsubscribe();
const sub4 = source$.subscribe();
sub4.unsubscribe();

No matter how many subscriptions you make, only one log statement will appear.

If you want the cache to expire based on time, remove the never() and try this instead:

const source$ = Observable.timer(0, expirationTimeout)
    .switchMap(() => api$)
    .shareReplay(1);

Keep in mind that this creates a hot stream that will continue querying the API until all subscribers unsubscribe – watch out for memory leaks.


Just a heads up, using Observable.never() trick may only work with newer versions of rxjs due to this fixed bug. The same applies to the solution using timers.

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

Issue encountered: Jest database test does not end when using testcontainers

I am currently in the process of testing my database within my Node application written in Typescript. I have implemented Postgres 15 and Testcontainers for this purpose. Strangely, my code functions correctly when executed manually, with all clients being ...

The input of 'Response' does not match the expected type of 'string'

I am currently working on a project that involves retrieving books from the Google Book API using Angular 4. Although I am still learning Angular, I am facing challenges in understanding how to read the JSON response. During my research online, I came acr ...

How to dynamically disable a checkbox in Angular reactive forms depending on the value of another checkbox

I am attempting to deactivate the checkbox based on the value of other checkboxes. Only one of them can be activated at a time. When trying to activate one of the checkboxes, I encounter an issue where the subscribed value is being repeated multiple times ...

Tips for retrieving the generated ID from the server immediately following form submission using the Post method in TypeScript

When submitting a long-form, it is important to ensure that no data is lost. Users should be able to stay on the same page to make any necessary changes after clicking the submit button. I need to receive the unique id generated by the server upon submissi ...

When restarting the React application, CSS styles disappear from the page

While developing my React application, I encountered a problem with the CSS styling of the Select component from Material UI. Specifically, when I attempt to remove padding from the Select component, the padding is successfully removed. However, upon refre ...

Alternative image loading in a figure element

I'm currently in the process of putting together an image gallery using Angular 4.3.0, where images are displayed as background images within figure elements rather than img tags. The images are initially resized to smaller dimensions before being use ...

What is the best way to insert CSS code into a custom Vue directive file?

I need a solution that applies a gradient background to the parent div in case an image fails to load. I've attempted to create a directive for this purpose: export default { bind(el: any, binding: any) { try { ..... ...

The latest update for angular-devkit/build-angular (version 0.13.4) has brought a new issue with the configuration output. It seems that there is an unrecognized property

After upgrading a project from angular-devkit/build-angular v0.11.4 to v0.13.4, an error is now appearing: Invalid configuration object. Webpack has been initialised using a setup that does not align with the API schema. - configuration.output contains a ...

Strategies for reducing the number of ngIf statements in Angular's template

I'm seeking advice on how to avoid using multiple *ngIf in templates. For instance, in a component's template, depending on the route, I need to display various elements like so: <div *ngIf="route == 'page1'">Title for page 1< ...

Angular Components: Achieving Full Height Issue with TabsResolved

I'm facing a challenge in making my tab fill the full height of the content underneath it. I've tried different solutions like setting height: 100%, but nothing seems to be working. Here is the HTML code: <mat-tab-group [selectedIndex]=" ...

Angular 2: Musing on the potential of Hot Module Replacement and the power of @ngrx/store

If you're just getting started, this link might be helpful: understanding the purpose of HMR. When it comes to managing and designing large projects, I'm still in the early stages and haven't grown a wise beard yet. So, I'm seeking adv ...

Is it possible for Typescript to allow extracted interfaces while excluding properties from another interface?

I've been searching for information on the specific features of this. Despite my efforts on Google, I have been unable to find the details. Any help would be greatly appreciated! interface Numbers { number: number; number2: number; number ...

Encountering Vue linting errors related to the defineEmits function

I am encountering an issue with the linting of my Vue SPA. I am using the defineEmits function from the script setup syntactic sugar (https://v3.vuejs.org/api/sfc-script-setup.html). The error messages are perplexing, and I am seeking assistance on how to ...

Exploring the differences between Office Fabric UI's I[component]StyleProp and the I[component]Styles interface

The Office Fabric UI documentation provides two interfaces for each component, such as https://developer.microsoft.com/en-us/fabric#/components/nav includes INavStyleProps interface and INavStyles interface A component that implements INavStyleProps ...

Sync user information when alterations are made on a different device

As I create a Discord clone using Next.js, I've encountered an issue where when a server is deleted, another client can still see and use the server until the page is reloaded. When testing out the official Discord web app, changes seemed to happen in ...

Guide to integrating Gandi.Net API with Node.js in server.js

I'm a beginner in Node.Js and I'm currently working on creating a Mean Stack application. One of the things I need to do is call a 3rd party API, specifically Gandi.Net, from my Node.js code. My Node.Js and Express Application are being used to ...

Creating a dynamic popup/modal in Angular 7 using a service

As a newcomer to Angular, I find myself with a question regarding services. In the past, when working with other languages, I would create self-contained components that could be called from anywhere within the application. Currently, my need for popup di ...

A guide on how to add the chosen item within the same tag using Angular 2

I am trying to choose an item and then add the same item within the same tag, similar to how tags are selected when asking a question on Stack Overflow. This is my template: <div class="form-group" *ngFor="let i of show"> <label for="examp ...

The stream-chat-js package is causing an Angular compile error due to the absence of an exported member named 'Client'

Encountering an issue while attempting to compile an Angular application using the Node version of Stream Chat package. ERROR in node_modules/getstream/types/getstream/index.d.ts(12,11): error TS2694: Namespace '"/Users/.../app/node_modules/stream-c ...

Developing the headers for a service using React.js

As someone new to ReactJs, I'm curious about the various methods we can use to include Headers in our service Url before making a call. While I'm familiar with how GET/POST Calls are made in angular Js after including headers, I'd like to l ...