Skip to content

Commit d04498b

Browse files
authored
Merge pull request #92 from gnom7/ISSUE-90-rxjs
ISSUE-90: Better handling of synchronous (sequential) requests
2 parents 4dc80cd + de85cd0 commit d04498b

File tree

2 files changed

+59
-35
lines changed

2 files changed

+59
-35
lines changed

src/lib/components/ng-http-loader.component.ts

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
*/
99

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

2828
@Input()
2929
public backgroundColor: string;
@@ -40,16 +40,21 @@ export class NgHttpLoaderComponent implements OnDestroy, OnInit {
4040
@Input()
4141
public minDuration = 0;
4242
@Input()
43+
public extraDuration = 0;
44+
@Input()
4345
public entryComponent: any = null;
4446

4547
constructor(private pendingInterceptorService: PendingInterceptorService, private spinnerVisibilityService: SpinnerVisibilityService) {
48+
const [showSpinner, hideSpinner] = partition((h: boolean) => h)(this.pendingInterceptorService.pendingRequestsStatus$);
49+
4650
this.subscriptions = merge(
47-
this.pendingInterceptorService.pendingRequestsStatus$.pipe(
48-
debounce(h => this.handleDebounceDelay(h)),
49-
delayWhen(h => this.handleMinDuration(h))
51+
showSpinner.pipe(debounce(() => timer(this.debounceDelay))),
52+
showSpinner.pipe(
53+
switchMap(() => hideSpinner.pipe(debounce(() => this.getHiddingTimer())))
5054
),
5155
this.spinnerVisibilityService.visibilityObservable$,
5256
)
57+
.pipe(distinctUntilChanged())
5358
.subscribe(h => this.handleSpinnerVisibility(h));
5459
}
5560

@@ -68,13 +73,13 @@ export class NgHttpLoaderComponent implements OnDestroy, OnInit {
6873
}
6974
}
7075

71-
private initFilters() {
76+
private initFilters(): void {
7277
this.initFilteredUrlPatterns();
7378
this.initFilteredMethods();
7479
this.initFilteredHeaders();
7580
}
7681

77-
private initFilteredUrlPatterns() {
82+
private initFilteredUrlPatterns(): void {
7883
if (!(this.filteredUrlPatterns instanceof Array)) {
7984
throw new TypeError('`filteredUrlPatterns` must be an array.');
8085
}
@@ -86,45 +91,29 @@ export class NgHttpLoaderComponent implements OnDestroy, OnInit {
8691
}
8792
}
8893

89-
private initFilteredMethods() {
94+
private initFilteredMethods(): void {
9095
if (!(this.filteredMethods instanceof Array)) {
9196
throw new TypeError('`filteredMethods` must be an array.');
9297
}
9398
this.pendingInterceptorService.filteredMethods = this.filteredMethods;
9499
}
95100

96-
private initFilteredHeaders() {
101+
private initFilteredHeaders(): void {
97102
if (!(this.filteredHeaders instanceof Array)) {
98103
throw new TypeError('`filteredHeaders` must be an array.');
99104
}
100105
this.pendingInterceptorService.filteredHeaders = this.filteredHeaders;
101106
}
102107

103-
private handleSpinnerVisibility(hasPendingRequests: boolean): void {
104-
this.isSpinnerVisible = hasPendingRequests;
105-
}
106-
107-
private handleDebounceDelay(hasPendingRequests: boolean): Observable<number | never> {
108-
if (hasPendingRequests && !!this.debounceDelay) {
109-
return timer(this.debounceDelay);
110-
}
111-
112-
return EMPTY;
113-
}
114-
115-
private handleMinDuration(hasPendingRequests: boolean): Observable<number> {
116-
if (hasPendingRequests || !this.minDuration) {
117-
if (this.shouldInitStartTime()) {
118-
this.startTime = Date.now();
119-
}
120-
121-
return timer(0);
108+
private handleSpinnerVisibility(showSpinner: boolean): void {
109+
const now = Date.now();
110+
if (showSpinner && this.visibleUntil <= now) {
111+
this.visibleUntil = now + this.minDuration;
122112
}
123-
124-
return timer(this.minDuration - (Date.now() - this.startTime));
113+
this.isSpinnerVisible = showSpinner;
125114
}
126115

127-
private shouldInitStartTime(): boolean {
128-
return !this.isSpinnerVisible;
116+
private getHiddingTimer(): Observable<number> {
117+
return timer(Math.max(this.extraDuration, this.visibleUntil - Date.now()));
129118
}
130119
}

src/test/components/ng-http-loader.component.spec.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,15 +518,50 @@ describe('NgHttpLoaderComponent', () => {
518518
const secondRequest = httpMock.expectOne('/fake2');
519519
expect(component.isSpinnerVisible).toBeTruthy();
520520

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

526526
// 500 ms later, the second http request ends. The spinner should be hidden
527-
// Total time spent visible (1000+200+900+500==2600 > minDuration)
527+
// Total time spent visible (1000+200+1400==2600 > minDuration)
528528
tick(500);
529529
secondRequest.flush({});
530+
tick();
531+
expect(component.isSpinnerVisible).toBeFalsy();
532+
}
533+
)));
534+
535+
it('should handle the extra spinner duration for multiple HTTP requests ran one after the others', fakeAsync(inject(
536+
[HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
537+
component.extraDuration = 10;
538+
539+
function runQuery(url: string): Observable<any> {
540+
return http.get(url);
541+
}
542+
543+
runQuery('/fake').subscribe();
544+
const firstRequest = httpMock.expectOne('/fake');
545+
546+
tick(1000);
547+
expect(component.isSpinnerVisible).toBeTruthy();
548+
549+
// the first HTTP request is finally over, the spinner is still visible for at least 10ms
550+
firstRequest.flush({});
551+
tick(5);
552+
expect(component.isSpinnerVisible).toBeTruthy();
553+
554+
// But 5 ms after the first HTTP request has finished, a second HTTP request has been launched
555+
runQuery('/fake2').subscribe();
556+
const secondRequest = httpMock.expectOne('/fake2');
557+
558+
// After 700ms, the second http request ends. The spinner is still visible
559+
tick(700);
560+
secondRequest.flush({});
561+
expect(component.isSpinnerVisible).toBeTruthy();
562+
563+
// 10ms later, the spinner should be hidden (extraDuration)
564+
tick(10);
530565
expect(component.isSpinnerVisible).toBeFalsy();
531566
}
532567
)));

0 commit comments

Comments
 (0)