Skip to content

Commit

Permalink
Merge pull request #92 from gnom7/ISSUE-90-rxjs
Browse files Browse the repository at this point in the history
ISSUE-90: Better handling of synchronous (sequential) requests
  • Loading branch information
mpalourdio committed Sep 16, 2018
2 parents 4dc80cd + de85cd0 commit d04498b
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 35 deletions.
55 changes: 22 additions & 33 deletions src/lib/components/ng-http-loader.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
*/

import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { EMPTY, merge, Observable, Subscription, timer } from 'rxjs';
import { debounce, delayWhen } from 'rxjs/operators';
import { merge, Observable, Subscription, timer } from 'rxjs';
import { debounce, distinctUntilChanged, partition, switchMap } from 'rxjs/operators';
import { PendingInterceptorService } from '../services/pending-interceptor.service';
import { SpinnerVisibilityService } from '../services/spinner-visibility.service';
import { Spinkit } from '../spinkits';
Expand All @@ -23,7 +23,7 @@ export class NgHttpLoaderComponent implements OnDestroy, OnInit {
public isSpinnerVisible: boolean;
public spinkit = Spinkit;
private subscriptions: Subscription;
private startTime: number;
private visibleUntil: number = Date.now();

@Input()
public backgroundColor: string;
Expand All @@ -40,16 +40,21 @@ export class NgHttpLoaderComponent implements OnDestroy, OnInit {
@Input()
public minDuration = 0;
@Input()
public extraDuration = 0;
@Input()
public entryComponent: any = null;

constructor(private pendingInterceptorService: PendingInterceptorService, private spinnerVisibilityService: SpinnerVisibilityService) {
const [showSpinner, hideSpinner] = partition((h: boolean) => h)(this.pendingInterceptorService.pendingRequestsStatus$);

this.subscriptions = merge(
this.pendingInterceptorService.pendingRequestsStatus$.pipe(
debounce(h => this.handleDebounceDelay(h)),
delayWhen(h => this.handleMinDuration(h))
showSpinner.pipe(debounce(() => timer(this.debounceDelay))),
showSpinner.pipe(
switchMap(() => hideSpinner.pipe(debounce(() => this.getHiddingTimer())))
),
this.spinnerVisibilityService.visibilityObservable$,
)
.pipe(distinctUntilChanged())
.subscribe(h => this.handleSpinnerVisibility(h));
}

Expand All @@ -68,13 +73,13 @@ export class NgHttpLoaderComponent implements OnDestroy, OnInit {
}
}

private initFilters() {
private initFilters(): void {
this.initFilteredUrlPatterns();
this.initFilteredMethods();
this.initFilteredHeaders();
}

private initFilteredUrlPatterns() {
private initFilteredUrlPatterns(): void {
if (!(this.filteredUrlPatterns instanceof Array)) {
throw new TypeError('`filteredUrlPatterns` must be an array.');
}
Expand All @@ -86,45 +91,29 @@ export class NgHttpLoaderComponent implements OnDestroy, OnInit {
}
}

private initFilteredMethods() {
private initFilteredMethods(): void {
if (!(this.filteredMethods instanceof Array)) {
throw new TypeError('`filteredMethods` must be an array.');
}
this.pendingInterceptorService.filteredMethods = this.filteredMethods;
}

private initFilteredHeaders() {
private initFilteredHeaders(): void {
if (!(this.filteredHeaders instanceof Array)) {
throw new TypeError('`filteredHeaders` must be an array.');
}
this.pendingInterceptorService.filteredHeaders = this.filteredHeaders;
}

private handleSpinnerVisibility(hasPendingRequests: boolean): void {
this.isSpinnerVisible = hasPendingRequests;
}

private handleDebounceDelay(hasPendingRequests: boolean): Observable<number | never> {
if (hasPendingRequests && !!this.debounceDelay) {
return timer(this.debounceDelay);
}

return EMPTY;
}

private handleMinDuration(hasPendingRequests: boolean): Observable<number> {
if (hasPendingRequests || !this.minDuration) {
if (this.shouldInitStartTime()) {
this.startTime = Date.now();
}

return timer(0);
private handleSpinnerVisibility(showSpinner: boolean): void {
const now = Date.now();
if (showSpinner && this.visibleUntil <= now) {
this.visibleUntil = now + this.minDuration;
}

return timer(this.minDuration - (Date.now() - this.startTime));
this.isSpinnerVisible = showSpinner;
}

private shouldInitStartTime(): boolean {
return !this.isSpinnerVisible;
private getHiddingTimer(): Observable<number> {
return timer(Math.max(this.extraDuration, this.visibleUntil - Date.now()));
}
}
39 changes: 37 additions & 2 deletions src/test/components/ng-http-loader.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,15 +518,50 @@ describe('NgHttpLoaderComponent', () => {
const secondRequest = httpMock.expectOne('/fake2');
expect(component.isSpinnerVisible).toBeTruthy();

// After 900ms, the second http request ends. The spinner should
// After 900ms, the spinner should
// still be visible because the second HTTP request is still pending
tick(900);
expect(component.isSpinnerVisible).toBeTruthy();

// 500 ms later, the second http request ends. The spinner should be hidden
// Total time spent visible (1000+200+900+500==2600 > minDuration)
// Total time spent visible (1000+200+1400==2600 > minDuration)
tick(500);
secondRequest.flush({});
tick();
expect(component.isSpinnerVisible).toBeFalsy();
}
)));

it('should handle the extra spinner duration for multiple HTTP requests ran one after the others', fakeAsync(inject(
[HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
component.extraDuration = 10;

function runQuery(url: string): Observable<any> {
return http.get(url);
}

runQuery('/fake').subscribe();
const firstRequest = httpMock.expectOne('/fake');

tick(1000);
expect(component.isSpinnerVisible).toBeTruthy();

// the first HTTP request is finally over, the spinner is still visible for at least 10ms
firstRequest.flush({});
tick(5);
expect(component.isSpinnerVisible).toBeTruthy();

// But 5 ms after the first HTTP request has finished, a second HTTP request has been launched
runQuery('/fake2').subscribe();
const secondRequest = httpMock.expectOne('/fake2');

// After 700ms, the second http request ends. The spinner is still visible
tick(700);
secondRequest.flush({});
expect(component.isSpinnerVisible).toBeTruthy();

// 10ms later, the spinner should be hidden (extraDuration)
tick(10);
expect(component.isSpinnerVisible).toBeFalsy();
}
)));
Expand Down

0 comments on commit d04498b

Please sign in to comment.