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

The promise object is displayed instead of the actual data retrieved from the API call

I am currently working on fetching data from an API and showcasing the name of the returned data on the front end. This function successfully retrieves the data through an API call: async function retrieveData(url){ var _data; let response = await fetch( ...

Exclude the key-value pair for any objects where the value is null

Is there a way to omit one key-value pair if the value is null in the TypeScript code snippet below, which creates a new record in the Firestore database? firestore.doc(`users/${user.uid}`).set({ email: user.email, name: user.displayName, phone: ...

Utilizing Angular formControl validators that are interdependent on other formControls

I am currently working on creating a form that includes two dates: dateFrom and dateTo. The validation requirement is that dateFrom must not come after dateTo, and dateTo must not come before dateFrom. To meet this condition, I have set up a form group wi ...

Struggling to maintain consistent updates on a child element while using the @Input property

I need to ensure that the data source in loans.component.ts is updated whenever a new loan is submitted from loan-form.component.ts. Therefore, in loan-form.component.ts, I have the following function being called when the form is submitted: onSubmit() { ...

Is there a way to attach numerical values to CSS in order to generate a timeline effect?

My goal is to arrange div elements on a timeline based on an array of floats ranging from 0 to 1. The idea is to have the elements positioned along the timeline according to these float values, with 0 representing the beginning and 1 indicating the end. ...

Utilize Firebase for Playwright to efficiently implement 'State Reuse' and 'Authentication Reuse'

In my testing environment, I want to eliminate the need for repeated login actions in each test run. My approach involves implementing 'Re-use state' and 'Re-use Authentication', but I've encountered a challenge with Firebase using ...

The value from select2 dropdown does not get populated in my article in Angular

I am attempting to link the selected value in a dropdown menu to an article, with a property that matches the type of the dropdown's data source. However, despite logging my article object, the property intended to hold the selected dropdown value app ...

Sorting JSON arrays in Typescript or Angular with a custom order

Is there a way to properly sort a JSON array in Angular? Here is the array for reference: {"title":"DEASDFS","Id":11}, {"title":"AASDBSC","Id":2}, {"title":"JDADKL","Id":6}, {"title":"MDASDNO","Id":3}, {"title":"GHFASDI","Id":15}, {"title":"HASDFAI","Id": ...

Ensure that TypeScript compiled files are set to read-only mode

There is a suggestion on GitHub to implement a feature in tsc that would mark compiled files as readonly. However, it has been deemed not feasible and will not be pursued. As someone who tends to accidentally modify compiled files instead of the source fil ...

A guide on loading modules dynamically using React and Typescript from a server

I am working on a React / Typescript / Webpack / React-Router application that contains some large JS modules. Currently, I have two bundles (common.js and app.js) included on every page, with common.js being a CommonsChunkPlugin bundle. However, there is ...

Exploring NestJS: Leveraging the @Body() Decorator to Retrieve Request Body Data

import { Controller, Post, Body } from '@nestjs/common'; import { MyService } from 'my.service'; import { MyDto } from './dto/my.dto'; @Controller('my-route') export class MyController { constructor(private rea ...

Is there a way for me to manually manipulate the advancement of the progress bar from @ngx-progressbar/core in Angular5/Ionic4?

I've been working on implementing a progress bar into my application using the @ngx-progressbar/core library. However, I'm facing an issue where I can't seem to control its progress effectively. Whenever I try to increase the progress increm ...

Error in JSON format detected by Cloudinary in the live environment

For my upcoming project in Next.js, I have integrated a Cloudinary function to handle file uploads. Here is the code snippet: import { v2 as cloudinary, UploadApiResponse } from 'cloudinary' import dotenv from 'dotenv' dotenv.config() ...

Error in typescript: The property 'exact' is not found in the type 'IntrinsicAttributes & RouteProps'

While trying to set up private routing in Typescript, I encountered the following error. Can anyone provide assistance? Type '{ exact: true; render: (routerProps: RouterProps) => Element; }' is not compatible with type 'IntrinsicAttribu ...

Obtaining images from the backend within a component's TypeScript file in a MEAN stack application is a

In my component.ts file, I am looking to retrieve images from the backend/images folder as Image objects. The paths of these images are stored in the database. this.productsService.getProduct(this.editId).subscribe(productData => { this.name = prod ...

One typical approach in React/JavaScript for monitoring the runtime of every function within a program

Experimenting with different techniques such as performance.now() or new Date().getTime() has been done in order to monitor the processing time of every function/method. However, specifying these methods within each function for time calculation purposes h ...

Issue with border radius in MUI 5 affecting table body and footer elements

Currently, I am diving into a new project utilizing React version 18.2 and MUI 5.10.3 library. My main task involves designing a table with specific styles within one of the components. The table header should not display any border lines. The table body ...

Playing around with TypeScript + lambda expressions + lambda tiers (AWS)

Having trouble importing modules for jest tests in a setup involving lambdas, lambda layers, and tests. Here is the file structure: backend/ ├─ jest.config.js ├─ package.json ├─ babel.config.js ├─ layers/ │ ├─ tsconfig.json │ ├ ...

Determine the data type of the value for the mapped type

Is there a way to access the value of a type like the following: interface ParsedQs { [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[] } I am looking for a method to retrieve the ParsedQsValue type without directly duplicating it from ...

The interface "App" is not properly implemented by the class "FirebaseApp" causing an error

My attempt to set up AngularCLI and Firebase led to encountering this issue... How should I proceed? ERROR in node_modules/angularfire2/app/firebase.app.module.d.ts(5,22): error TS2420: Class 'FirebaseApp' does not properly implement interface ...