Tips for preventing a promise from being executed more than once within an observable while being subscribed to a BehaviorSubject

I've defined a class called Store with the following structure:

import { BehaviorSubject, Observable } from 'rxjs'

export abstract class Store<T> {
  private state: BehaviorSubject<T> = new BehaviorSubject((undefined as unknown) as T)

  get(): Observable<T> {
    return this.state.asObservable()
  }

  set(nextState: T) {
    return this.state.next(nextState)
  }

  value() {
    return this.state.getValue()
  }

  patch(params: Partial<T>) {
    this.set({ ...this.value(), ...params })
  }

  abstract create(): void
}

Now let's take a look at my InstallationStore:

import { Store } from '../../store/store'
import { Installation } from '../domain/installation/installation'
import { from, Observable } from 'rxjs'
import { GetActiveInstallationUseCase } from '../../../features/planning/application/get-active-installation-use-case'
import { Injectable } from '@angular/core'
import { map, switchMap } from 'rxjs/operators'
import { LoginStore } from '../../../features/login/application/login-store'

interface State {
  activeInstallation: Installation
}

@Injectable({
  providedIn: 'root'
})
export class InstallationStore extends Store<State> {
  constructor(
    private readonly getActiveInstallationUseCase: GetActiveInstallationUseCase,
    private readonly loginStore: LoginStore
  ) {
    super()
    this.create()
  }

  create(): void {
    this.set({
      activeInstallation: {
        isDefault: true,
        productionProfile: 'baz',
        incomingProfile: 'foo',
        id: 1,
        energeticRole: 'bar',
        name: ''
      }
    })
  }

  get(): Observable<State> {
    return this.loginStore
      .get()
      .pipe(
        switchMap(() => from(this.getActiveInstallationUseCase.execute()).pipe(map(x => ({ activeInstallation: x }))))
      )
  }
}

The problem I'm facing is that the InstallationStore is subscribed to the get observable twice in different components, causing the getActiveInstallationUseCase to run twice. The getActiveInstallationUseCase.execute() method returns a Promise, and I want it to execute only once when the user logs in.

I attempted to use the share() operator without success as shown below:

get(): Observable<State> {
    return this.loginStore
      .get()
      .pipe(
        switchMap(() => from(this.getActiveInstallationUseCase.execute()).pipe(map(x => ({ activeInstallation: x }))),
        share()
      )
  }

And also tried:

get(): Observable<State> {
    return this.loginStore
      .get()
      .pipe(
        switchMap(() => from(this.getActiveInstallationUseCase.execute()).pipe(map(x => ({ activeInstallation: x }))), share()),

      )
  }

Despite these attempts, the method still runs twice. I have verified that this.loginStore.get() emits an event just once and even tried replacing share with shareReplay but couldn't resolve the issue.


I created a demo of the issue here. Currently, the promise is being called 4 times, whereas I want it to be executed only twice. While using the share() operator solves the problem in the demo, it doesn't work as expected in my actual code. Why is that happening?

Answer №1

Consider incorporating the rxjs take method in your code, like this:

 fetch(): Observable<Data> {
    return this.dataService
      .fetch()
      .pipes(
        take(1),
        switchMap(() => from(this.getDataUseCase.execute()).pipe(map(y => ({ newData: y }))))
      )
  }

Answer №2

Upon delving deeper into RxJS, I discovered a misconception regarding how to share a subscription. The issue lay within this portion of code:

get(): Observable<State> {
    return this.loginStore
      .get()
      .pipe(
        switchMap(() => from(this.getActiveInstallationUseCase.execute() /* HERE */).pipe(map(x => ({ activeInstallation: x }))))
      )
  }

The execute function was actually returning a new observable each time it was called. Despite having a way to share all use cases in place, there was no actual sharing occurring due to the creation of new observables with each execution of .execute().

To address this issue, I implemented a cache system for observables. Given that all my use cases inherit from the same class, I established a chain of responsibility. If an observable had been executed before, it would be shared.

Here is the base use case class:

import { Observable } from 'rxjs'
import { dependencyTree } from '../../dependency-tree'

export abstract class UseCase<Param, Result> {
  abstract readonly: boolean

  abstract internalExecute(param: Param): Observable<Result>

  execute(param: Param): Observable<Result> {
    const runner = dependencyTree.runner
    return runner.run(this, param) as Observable<Result>
  }
}

An example of a use case:

import { Observable } from 'rxjs'
import { GameRepository } from '../domain/game-repository'
import { Id } from '../../../core/id'
import { map } from 'rxjs/operators'
import { Query } from '../../../core/use-case/query'

type Params = { id: Id }

export class HasGameStartedQry extends Query<boolean, Params> {
  constructor(private readonly gameRepository: GameRepository) {
    super()
  }

  internalExecute({ id }: Params): Observable<boolean> {
    return this.gameRepository.find(id).pipe(map(x => x?.start !== undefined ?? false))
  }
}

This is the runner:

import { ExecutorLink } from './links/executor-link'
import { Observable } from 'rxjs'
import { LoggerLink } from './links/logger-link'
import { Context } from './context'
import { UseCase } from './use-case'
import { CacheLink } from './links/cache-link'

export class Runner {
  chain = this.cacheLink.setNext(this.executorLink.setNext(this.loggerLink))

  constructor(
    private readonly executorLink: ExecutorLink,
    private readonly loggerLink: LoggerLink,
    private readonly cacheLink: CacheLink
  ) {}

  run(useCase: UseCase<unknown, unknown>, param?: unknown): Observable<unknown> {
    const context = Context.create({ useCase, param })
    this.chain.next(context)
    return context.observable!
  }
}

The cache of observables, implemented as part of the chain:

import { BaseLink } from './base-link'
import { Context } from '../context'
import { Observable } from 'rxjs'

export class CacheLink extends BaseLink {
  private readonly cache = new Map<string, Observable<unknown>>()

  next(context: Context): void {
    if (context.param !== undefined) {
      this.nextLink.next(context)
      return
    }

    if (!this.cache.has(context.useCase.constructor.name)) {
      this.nextLink.next(context)
      this.cache.set(context.useCase.constructor.name, context.observable)
    }

    context.observable = this.cache.get(context.useCase.constructor.name)!
  }
}

And here is how I share the observables using the ExecutorLink:

import { BaseLink } from './base-link'
import { Context } from '../context'
import { share } from 'rxjs/operators'

export class ExecutorLink extends BaseLink {
  next(context: Context): void {
    if (!context.hasSetObservable) {
      const observable = context.useCase.internalExecute(context.param)
      if (context.useCase.readonly) {
        context.observable = observable.pipe(share())
      } else {
        context.observable = observable
      }
    }
    this.nextLink.next(context)
  }
}

All components of this code can be accessed in this repository: https://github.com/cesalberca/who-am-i. Any suggestions on enhancing the structure are highly welcomed!

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

How to transition from using a CDN to NPM for implementing the Google Maps JavaScript MarkerClusterer?

Currently integrating Google Maps JavaScript MarkerClusterer from CDN, I am considering transitioning to the NPM version for Typescript checking in my JavaScript files. However, I am encountering difficulties understanding how to make this switch. The docu ...

Using Node.js to send a response only after every promise has been resolved

I am currently working on a NodeJS Express route where I have encountered an issue. In this route, a function is called multiple times, each returning a Promise. The values from these Promises are then added to an Array and sent back to the client using re ...

What steps are needed to switch colors after a loop has been clicked?

I have a loop setup like this: data: { show: false } .test { hight: 10px; background-color: red; } .test2 { hight: 15px; } <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script> <div v-for="value in da ...

Discovering the origins of the node.js native modules and delving into the intricacies of typed modules

I am using a Windows machine and trying to locate where node fetches the source code for native modules. On my system, I can only find the @types file which contains "Typed Only" modules. For example, the module "assert" is available in the master/lib fold ...

What is the process for generating a new object by outputting key-value pairs?

I have an array that I'm trying to use to create a new object with specific keys and values. const arr = [ { name: 'ab', key: '584577', }, { name: 'cd', key: '344926', }, { name: &a ...

What is the best method for fetching the values of a select element in React.js?

I'm struggling to retrieve the value of a selected element in a dropdown list. I've tried debugging it, but haven't been able to get the value. I attempted to console log e.target.value, but unfortunately, it didn't work. Any thoughts o ...

Making AngularJS 'PUT' requests: The process of submitting only the data in a form

I am facing an issue while updating user data in Angular. When I send a 'PUT' request, the entire user $scope is being sent instead of only the fields visible on the form. To retrieve and update the data, I am using a Factory. Below is my edit f ...

PrismaClient is currently incompatible with this browser environment and has been optimized for use in an unknown browser when performing updates

While attempting to update a single field in my database using server-actions and tanstackQuery, I encountered the following error message: Error: PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in ...

Issue with Videogular: hh:mm:ss Date filter not functioning properly

I am currently working on integrating Videogular into my audio player application. Below are the settings provided in the example code on this particular page: <vg-time-display>{{ currentTime | date:'mm:ss' }}</vg-time-display> ...

Troubleshooting the issue with the htmlFor attribute

I have encountered an issue with creating radio buttons and labels using JavaScript. Despite adding the 'for' attribute in the label using 'htmlFor', it does not apply to the actual DOM Element. This results in the label not selecting t ...

Is it possible to use a Backbone Model for an unconventional HTTP POST request that isn't

After going through the documentation at and , I tried to make an HTTP POST request to fetch some JSON data for my model. However, due to the services not being RESTful, I ended up using a POST request instead of a GET request. The code snippet I have co ...

Testing the subscription feature with unit tests

I am currently testing the reception of an array of people to ensure consistent response types, allowing for detection of any changes in the object from the back-end. Interface Definition: export interface People { peopleDtos: [{ id: number, ...

Deleting a key from a type in TypeScript using subtraction type

I am looking to create a type in TypeScript called ExcludeCart<T>, which essentially removes a specified key (in this case, cart) from the given type T. For example, if we have ExcludeCart<{foo: number, bar: string, cart: number}>, it should re ...

Having trouble sending a POST request to a Node.js HTTP Webserver unless it's from the localhost

I am currently facing an issue while attempting to transfer text/data from a client browser to a Node.js webserver. Whenever I try to use any IP address other than localhost, I encounter a "POST ERR_CONNECTION_TIMED_OUT" error. Below is the code snippet f ...

Angular - Ensuring the Quality of Your Code

After creating a new project in Angular using the command "ng new app" and running "npm test," I successfully completed 3 tests. How can I view these tests in Test Explorer in Visual Studio? Do I need to open the project differently since I usually use "Op ...

How can you apply a class to a different element by hovering over one element?

Is there a way to darken the rest of the page when a user hovers over the menu bar on my website? I've been playing around with jQuery but can't seem to get it right. Any suggestions? I'm looking to add a class '.darken' to #conte ...

What is the method to execute a prototype function within another prototype?

I am facing an issue with my JavaScript class and need some help to resolve it. MyClass.prototype.foo = function() { return 0; } MyClass.prototype.bar = function() { return foo() + 1; } Unfortunately, when I try to run the program, it throws an ...

Problems with Ajax calls are preventing Internet Explorer9 and Internet Explorer10 from functioning properly

I am facing an issue with displaying item pages using Ajax. The function works perfectly in Chrome, but not in Internet Explorer. <script type="text/javascript"> function obtainInfo(str) { if (str=="") { document. ...

Determine the data type of a string without needing to convert it

Can you determine if a value is numeric without casting it? For example, how can you differentiate between 'abc' and '123' without converting them to a specific data type? While visually apparent that 'abc' is not numeric, t ...

Transforming Objects with THREE.js: Navigating the Order of Transformations

Currently learning THREE.js and facing a bit of a newbie issue. I have a JSON object with dynamic updates, containing data for 4 walls. The JSON structure is as follows: { ... walls: [{ start: { x : 0, y : ...