4
votes

The scenario:

I have a service that polls an URL every 2 seconds:

export class FooDataService {

...

  public provideFooData() {
    const interval = Observable.interval(2000).startWith(0);
    return interval
      .switchMap(() => this.http.get(this.requestUrl))
      .map(fooData => fooData.json())
  }
}

Now I have a component where I want to show this polled data:

export class FooComponent implements OnInit {

  constructor(private fooDataService: FooDataService) {}

  public fooData$: Observable<FooData>;

  ngOnInit() {
    this.fooData$ = this.fooDataService.provideFooData();
  }
}

In the component template I would use an async pipe to retrieve and pass the value along to a sub component:

<foo-data-viewer [data]="fooData$ | async"></foo-data-viewer>

The problem with this approach:

Implementing it like this is not testable with protractor (see my last question on this topic or this article). All code is executed within the ngZone and protractor will wait for all queued actions to be finished before continuing. But Observable.interval() will queue an unlimited number of actions, thus leading to a timeout in protractor.

The common solution:

The fix I read most often is to use runOutsideAngular like this:

export class FooComponent implements OnInit, OnDestroy {

  constructor(private ngZone: NgZone,
              private fooDataService: FooDataService) {}

  public fooData: FooData;
  public fooDataSubscription: Subscription<FooData>;

  ngOnInit() {

    this.ngZone.runOutsideAngular(() => {
      this.fooDataSubscription =
        this.fooDataService.provideFooData()
            .subscribe(
               fooData => this.ngZone.run(() => this.fooData = fooData)
            );
    });
  }

  ngOnDestroy(): void {
    this.fooDataSubscription.unsubscribe();
  }
}

By running the interval outside of the ngZone, protractor does not wait for the polling to finish before continuing and thus doesn't time out.

This means however:

  • I cannot use the async pipe, I have to subscribe manually.
  • I have to manage the subscription manually and clean it up myself when the component is destroyed.
  • I cannot keep the pretty and clean Observable-style and the code gets much more convoluted, especially if I have multiple polling services.

My question:

Is there a way to keep the functional rxjs style and keep working with the async pipe (or an equivalent) while running the interval outside of the ngZone?

I stumbled across this github project which looks like exactly what I want, but I couldn't get it to work.

What I need is a working example for leaving and reentering the zone like in my scenario without having to manage the subscription myself.

1
Someone wrote a nice Service, that seems to do the trick: github.com/angular/protractor/issues/…Chris

1 Answers

0
votes
import { Injectable } from '@angular/core';
import { interval, of, timer } from 'rxjs';
import { startWith, switchMap, delay, map, share, shareReplay } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class DataService {
  private fakeRequest$ = timer(1000).pipe(map(() => new Date().getTime()))
  private pollingData$ = interval(2000)
    .pipe(
      switchMap((index) => this.fakeRequest$),
      shareReplay(1)
    )
  constructor() { }

  public getData$() {
    return this.pollingData$;
  }
}

Stackblitz