Retrieve data from a web api at regular intervals using Angular observables and subscription

My Launch method is designed to start an engine by taking parameters and returning the instance name once started. After that, I need to periodically query another service every 2 seconds to check if the status has changed to either "Succeeded" or "Failed". I attempted to use a do while loop within the first subscription, but it did not work as expected.

instanceStatus: string = "Initialized";
instanceName:string = "InstanceName";

Launch(sessionId: string, projectName: string, f: string[]) {
    this.service.Launch(sessionId, projectName, this.f)
      .pipe(first())
      .subscribe(
        instanceName => {
          localStorage.setItem('instanceName', instanceName);
          this.instanceName = instanceName;
          setTimeout(() => {
            do {
              this.service.getEngineStatus(this.instanceName)
                .pipe(first())
                .subscribe(
                  status => {
                    this.instanceStatus = status;
                    console.log(status);
                    console.log(this.instanceStatus);
                    this.loadingService.showSpinner({ text: 'Modeling is running...' });
                    if (this.instanceStatus === "Succeeded") {
                      this.messageService.add({ severity: Severity.Success, summary: 'Fault modeling completed', detail: 'Via MessageService' });
                      this.messageService.clear();
                    }
                  }
                );
            } while (this.instanceStatus !== "Succeeded")
          }, 2000);
        }
      );
  }

 getEngineStatus(instanceName:string): Observable<string> {
    this.serviceUrl = URL + `?instance=` + instanceName;
    return this._http.get<string>(this.serviceUrl);
  }

Answer №1

If you haven't already, take a look at the interval creation operator. It could be just what you're searching for. I experimented with this operator and came up with the following code:

Execute(sessionId: string, projectName: string, operations: string[]) {
  this.service
    .Execute(sessionId, projectName, this.operations)
    .pipe(
      first(),
      tap((executionResult) => {
        localStorage.setItem('executionResult', executionResult);
        this.executionResult = executionResult;
      }),
      switchMap(() => interval(2000)),
      takeWhile(() => !['Completed', 'Failed'].includes(this.executionStatus)),
      tap(() => {
        this.loadingService.showSpinner({
          text: 'Processing operations...',
        });
      }),
      switchMap(() => this.service.getExecutionStatus(this.executionResult))
    )
    .subscribe((status) => {
      this.executionStatus = status;
      this.loadingService.hideSpinner();
      console.log(status);
      console.log(this.executionStatus);

      if (this.executionStatus === 'Completed') {
        this.messageService.add({
          severity: Severity.Success,
          summary: 'Operations executed successfully',
          detail: 'Via MessageService',
        });
        this.messageService.clear();
      }
    });
}

Answer №2

  1. It is recommended to avoid nested subscriptions and instead utilize higher order mapping operators such as switchMap.

  2. Instead of relying on traditional JS statements like for or while, consider using RxJS functions like interval or timer for polling purposes.

  3. Utilize the tap operator to handle side effects, such as storing data in local storage.

import { Observable, timer } from 'rxjs';
import { tap, finalize, switchMap, takeWhile } from 'rxjs/operators';

POLL_INTERVAL = 2000;
instanceStatus: string = "Initialized";
instanceName:string = "InstanceName";

this.Launch(sample, sample, sample).pipe(
  switchMap((instanceName: any) => 
    timer(0, POLL_INTERVAL).pipe(                                         // <-- start immediately and poll every 'n' secs
      switchMap(() => this.service.getEngineStatus(instanceName)),
      takeWhile((instanceStatus: any) => instanceStatus === 'Succeeded'), // <-- stop poll when status !== 'Succeeded'
      finalize(() => {                                                    // <-- run when polling stops
        this.messageService.add({
          severity: Severity.Success,
          summary: 'Fault modeling completed',
          detail: 'Via MessageService'
        });
        this.messageService.clear();
      })
    )
  ).subscribe({
    next: (instanceStatus: any) => this.instanceStatus = instanceStatus
  });
)

Launch(sessionId: string, projectName: string, f: string[]): Observable<any> {
  return this.service.Launch(sessionId, projectName, this.f).pipe(
    first(),
    tap((instanceName: any) => {
      localStorage.setItem('instanceName', instanceName);
      this.instanceName = instanceName;
    })
  );
}

Update (Thanks to @Liam):

  1. do has been replaced by the tap operator.
  2. The placement of .subscribe() within the pipe() function was incorrect and should be at the end after all operators. Please adjust the code accordingly.

Answer №3

Upon my observation, it seems that implementing something along these lines could lead you towards finding a solution.

The critical aspect here is the timer triggering a value every 2 seconds as long as status !== "Succeeded". Once status === "Succeeded", setting the second argument in takeWhile to true allows for the final emission prior to closing the timer (eliminating further checks every 2 seconds).

Everything else flows from this point.

launch(sessionId: string, projectName: string, f: string[]): void {
  this.service.launch(sessionId, projectName, f).pipe(

    first(),
    tap(instanceName => {
      localStorage.setItem('instanceName', instanceName);
      this.instanceName = instanceName;
    }),
    switchMap((instanceName:string) => timer(0,2000).pipe(
      exhaustMap(_ => this.service.getEngineStatus(instanceName).pipe(
        first()
      )),
      takeWhile((status:string) => status !== "Succeeded", true),
      map(status => ({instanceName, status}))
    ))

  ).subscribe(({instanceName, status}) => {
    this.loadingService.showSpinner({ text: 'Modeling is running...' });
    if (status === "Succeeded") {
      this.messageService.add({ 
        severity: Severity.Success, 
        summary: 'Fault modeling completed', 
        detail: 'Via MessageService' 
      });
      this.messageService.clear();
    }
  });
}

Update

Below is code that achieves your goal but is completely self-contained. It's not based on your code since I can't execute/test code without access.

The timers in the service simulate a delay between request and response.

type Status = "Succeeded" | "Other"

interface Service {
  // Pretending direct access to privateStatus isn't possible
  privateStatus: Status,
  launch: (sessionId: string) => Observable<string>,
  getStatus: (instanceName: string) => Observable<Status>
}

class ArbitraryClass {

  service: Service;
  readonly PING_INTERVAL = 2000; 

  constructor(){
    this.service = {
      privateStatus: "Other",
      launch: (sessionId: string) => timer(8000).pipe(
        mapTo(""),
        tap(status => this.service.privateStatus = "Succeeded"),
        filter(_ => false),
        startWith(`Hello ${sessionId}`)
      ),
      getStatus: _ => timer(500).pipe(mapTo(this.service.privateStatus))
    }
  }

  arbitraryInit(sessionId: string) {

    // Launch session, then query the status at each ping interval 
    // and display the results in the console.
    this.service.launch(sessionId).pipe(
      switchMap(instanceName => timer(0,this.PING_INTERVAL).pipe(
        exhaustMap(_ => this.service.getStatus(instanceName)),
        takeWhile(status => status !== "Succeeded", true),
        map(status => ({instanceName, status}))
      ))
    ).subscribe(console.log);

  }
  
}

new ArbitraryClass().arbitraryInit("ABCD_1234");

Output in the console when I ran this:

{ instanceName: 'Hello ABCD_1234', status: 'Other' }
{ instanceName: 'Hello ABCD_1234', status: 'Other' }
{ instanceName: 'Hello ABCD_1234', status: 'Other' }
{ instanceName: 'Hello ABCD_1234', status: 'Other' }
{ instanceName: 'Hello ABCD_1234', status: 'Succeeded' }

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

Utilizing Partial Types in TypeScript Getter and Setter Functions

Within the Angular framework, I have implemented a component input that allows for setting options, specifically of type IOptions. The setter function does not require complete options as it will be merged with default options. Therefore, it is typed as Pa ...

Using a callback function with a function outside the scope in Angular 6

I am currently utilizing DevExtreme components, and here is where the callback function is invoked within the HTML: <dxi-validation-rule type="custom" [validationCallback]="validationCallback" message="Email exists"> </dxi-validation-ru ...

The UploadFile Interface seems to be missing

Can someone clarify whether the @UploadedFile decorator's interface is predefined or if I need to define it myself? ...

Sending Angular 4 POST request to Java Spring Controller via HTTP

Hey there, I'm looking to pass a string from my Angular 4 post request to my Java Spring MVC controller and get its value returned. In the Angular 4 function: let body = 'example' http .post('favourite', body) .subscribe( ...

Customizing the placeholder text for each mat input within a formArray

I have a specific scenario in my mat-table where I need to display three rows with different placeholder text in each row's column. For example, test1, test2, and test3. What would be the most efficient way to achieve this? Code Example: <div form ...

ways to eliminate attributes using mapped types in TypeScript

Check out this code snippet: class A { x = 0; y = 0; visible = false; render() { } } type RemoveProperties<T> = { readonly [P in keyof T]: T[P] extends Function ? T[P] : never//; }; var a = new A() as RemoveProperties< ...

Guide to retrieving static information for forms with the use of reactive forms

I am encountering an issue with my reactive form. When I click the new button, a duplicate section appears but the options in the select field are not visible. Additionally, I receive errors as soon as the page loads: ERROR Error: Cannot find control with ...

Is there a way to adjust the opacity of a background image within a div?

I have a lineup of elements which I showcase in a grid format, here's how it appears: https://i.stack.imgur.com/JLCei.png Each element represents a part. The small pictures are essentially background-images that I dynamically set within my html. Up ...

What could be causing issues with my unit tests in relation to Angular Material tooltips?

I have a unique and specific issue with the following unit test code. It is very similar to another working file, but I am encountering an error related to mdTooltip from the Angular Material library. Here's the problematic portion of the code: Phant ...

How to retrieve a parameter value within the app component in Angular 2

Within my appcomponent, I have incorporated a dropdown functionality. Whenever the user selects an option from the dropdown, it loads a new page in the router outlet. However, if I refresh the page, the router loads correctly but the dropdown selection i ...

Is "await" considered as a reserved word in ReactJS when using TypeScript?

I am trying to implement async await in my code, but I keep getting an error that says await is a reserved word. Here is the snippet of my code: public componentDidMount() { this.startDrag(); } private startDrag = async () => { const eleme ...

I'm having trouble setting a value for an object with a generic type

I am attempting to set a value for the property of an object with generic typing passed into a function. The structure of the object is not known beforehand, and the function receives the property name dynamically as a string argument. TypeScript is genera ...

What is the optimal method for navigating through a complex nested object in Angular?

Looking to navigate through a nested object structure called purchase. Within this structure, there is a sub-array named purchaseProducts which contains another sub-array called products along with additional data. What methods do you suggest for efficien ...

Tips for implementing *ngIf within *ngFor in Angular 7

I'm facing an issue with my DocumentModel document where I have multiple items, but I want to exclude the 'content' item from being shown. I've tried checking the name of the item and using ngIf in Angular, but it still shows up: <p ...

The type of props injected by WithStyles

When working on my class component, I utilize material UI withStyles to inject classes as a property. export default withStyles(styles)(myComponent) In this process, const styles = ( (theme:Theme) => createStyles({className:CSS_PROPERTIES}) I am att ...

Building a dynamic hierarchical list in Angular 8 with recursive expansion and collapse functionality

I am attempting to construct a hierarchical expand/collapse list that illustrates a parent-child relationship. Initially, the parent nodes will be displayed. If they have children, a carat icon is shown; otherwise, a bullet icon appears. When the carat ico ...

Error encountered with the PrimeNG Angular2 Accordion component

https://i.sstatic.net/NqDIN.png I am currently utilizing the PrimeNG accordion module. After importing all components successfully, I encountered an issue with a newly created component. Despite verifying that all modules were imported correctly, I contin ...

Angular 6 - detecting clicks outside of a menu

Currently, I am working on implementing a click event to close my aside menu. I have already created an example using jQuery, but I want to achieve the same result without using jQuery and without direct access to the 'menu' variable. Can someon ...

When the button is clicked, a fresh row will be added to the table and filled with data

In my table, I display the Article Number and Description of werbedata. After populating all the data in the table, I want to add a new article and description. When I click on 'add', that row should remain unchanged with blank fields added below ...

What is the best way to validate the Click outside directive in Angular applications?

Exploring the click-outside directive for testing purposes. It seems that there is an issue with ignoring a specific div element while clicking outside. import { Directive, ElementRef, Output, EventEmitter, HostListener } from '@angular/core'; ...