skip to Main Content

I have the following class:

class BucketManager {
  private readonly bucketNames$ = new BehaviorSubject<string[] | null>(null)
  bucketNames = this.bucketNames$.asObservable().pipe(filter(bucketNames => !!bucketNames)) as Observable<string[]>

  constructor() {
    this.init()
  }

  async init() {
    const fetchedBuckets = await someExpensiveLongCalculation()
    this.bucketNames$.next(fetchedBuckets)
  }
}

Currently, the value of bucketNames$ is set immediately upon class initialization. How can I call init only once and only when bucketNames is first subscribed?

2

Answers


  1. Chosen as BEST ANSWER

    Eventually I came across defer and shareReplay.

    defer will execute a block of code upon subscription.
    shareReplay(1) will make sure that all observers will get the same value.
    from is used just to create an observable from the Promise returned by someExpensiveLongCalculation

    import { defer, shareReplay, from } from 'rxjs'
    
    readonly bucketNames = defer(() => from(someExpensiveLongCalculation())).pipe(
        shareReplay(1);
    );
    

  2. To achieve the behavior where the init method is only called once and only when the bucketNames observable is first subscribed to, you can use a combination of the defer operator and a flag to track whether the initialization has already been done. Here’s how you can modify your BucketManager class to achieve this:

    import { BehaviorSubject, Observable, defer } from 'rxjs';
    import { filter, take, tap } from 'rxjs/operators';
    
    class BucketManager {
      private readonly bucketNames$ = new BehaviorSubject<string[] | null>(null);
      bucketNames = defer(() => {
        if (!this.isInitialized) {
          this.init();
        }
        return this.bucketNames$.asObservable().pipe(
          filter(bucketNames => !!bucketNames),
          take(1),
          tap(() => {
            this.isInitialized = true;
          })
        );
      }) as Observable<string[]>;
    
      private isInitialized = false;
    
      constructor() {}
    
      async init() {
        const fetchedBuckets = await someExpensiveLongCalculation();
        this.bucketNames$.next(fetchedBuckets);
      }
    }

    In this implementation:

    The defer operator is used to delay the execution of the inner observable creation function until a subscriber subscribes to the bucketNames observable. This ensures that the init method is only called when the observable is first subscribed to.

    The isInitialized flag is used to keep track of whether the initialization has already been done. It’s initially set to false and gets updated to true after the initialization is complete.

    Inside the defer function, before initializing, it checks if isInitialized is false. If it’s false, the init method is called. Otherwise, if it’s already initialized, the initialization step is skipped.

    The take(1) operator ensures that the observable completes after emitting the first non-null value. This prevents subsequent subscribers from triggering the initialization again.

    The tap operator is used to set the isInitialized flag to true after the observable emits its first value, indicating that the initialization has been completed.

    With these modifications, the init method will only be called once and only when the bucketNames observable is first subscribed to. Subsequent subscribers will immediately receive the already initialized data.

    Source: chat-gpt

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search