Skip to content

Commit

Permalink
loading status service
Browse files Browse the repository at this point in the history
fixes: when several http requests are sent in parallel, loading status is true once the first one is done
  • Loading branch information
tdesvenain committed Oct 18, 2017
1 parent e2b5447 commit 3ebcfd8
Show file tree
Hide file tree
Showing 18 changed files with 249 additions and 32 deletions.
12 changes: 10 additions & 2 deletions CHANGELOG.md
@@ -1,3 +1,11 @@
# 1.2.5 (Unreleased)

## New features

- New LoadingService to manage loading status.
Global loading status is now robust on parallel requests.
[Thomas Desvenain]

# 1.2.4 (2017-10-11)

## New features
Expand All @@ -13,13 +21,13 @@

## Bug fixes

- fix error handling
- fix error handling

# 1.2.2 (2017-09-29)

## Bug fixes

- fix error handling
- fix error handling

# 1.2.1 (2017-09-29)

Expand Down
38 changes: 38 additions & 0 deletions docs/reference/services.rst
Expand Up @@ -158,3 +158,41 @@ The cache can't store more than as many entries as set on `CACHE_MAX_SIZE` prope
A `hits` property contains the hits statistics (number of hits by path).

Cache service is massively used by `resource` and `comments` service. All get requests are cached and all create/update/delete requests revokes cache.


Loading service
---------------

Loading service stores ids for what is currently loading. You declare here which loadings has started and finished.

The service provides observables that emits when loading status changes. This is useful when you want to display a reactive loader.

You give an id each 'thing' you mark as loaded using `start` method. You mark loading as finished using `finish` method.

`status` behavior subject changes when there is nothing loading left or if there is at least one thing loading.

`isLoading` method provides an observable that emits the loading status for a specific id.


.. code-block:: javascript
loading.status.subscribe((isLoading) => {
this.somethingIsLoading = isLoading;
});
loading.isLoading('the-data').subscribe((isLoading: boolean) => {
this.dataIsLoading = isLoading;
});
loading.start('the-data') // mark 'the-data' as loading
dataService.getData().subscribe((data: string[]) => {
loading.finish('the-data');
this.data = data;
}, (error) => {
loading.finish('the-data');
this.data = [];
this.error = error;
});
This service is used by AuthenticationService and ResourceService to mark a loading status when any http request is done.
43 changes: 24 additions & 19 deletions src/api.service.ts
Expand Up @@ -7,6 +7,7 @@ import 'rxjs/add/operator/catch';
import { AuthenticationService } from './authentication.service';
import { ConfigurationService } from './configuration.service';
import { Error, LoadingStatus } from './interfaces';
import { LoadingService } from './loading.service';

@Injectable()
export class APIService {
Expand All @@ -15,69 +16,73 @@ export class APIService {
{ loading: false }
);

constructor(
private authentication: AuthenticationService,
private config: ConfigurationService,
private http: HttpClient,
) { }
constructor(private authentication: AuthenticationService,
private config: ConfigurationService,
private http: HttpClient,
private loading: LoadingService) {
this.loading.status.subscribe((isLoading) => {
this.status.next({ loading: isLoading })
})
}

get(path: string): Observable<any> {
let url = this.getFullPath(path);
let headers = this.authentication.getHeaders();
this.status.next({ loading: true });
this.loading.start(`get-${path}`);
return this.http.get(url, { headers: headers }).map(res => {
this.status.next({ loading: false });
this.loading.finish(`get-${path}`);
return res;
})
.catch(this.error.bind(this));
.catch(this.error.bind(this));
}

post(path: string, data: Object): Observable<any> {
let url = this.getFullPath(path);
let headers = this.authentication.getHeaders();
this.status.next({ loading: true });
this.loading.start(`post-${path}`);
return this.http.post(url, data, { headers: headers }).map(res => {
this.status.next({ loading: false });
this.loading.finish(`post-${path}`);
return res;
})
.catch(this.error.bind(this));
.catch(this.error.bind(this));
}

patch(path: string, data: Object): Observable<any> {
let url = this.getFullPath(path);
let headers = this.authentication.getHeaders();
this.status.next({ loading: true });
this.loading.start(`patch-${path}`);
return this.http.patch(url, data, { headers: headers }).map(res => {
this.status.next({ loading: false });
this.loading.finish(`patch-${path}`);
return res;
})
.catch(this.error.bind(this));
.catch(this.error.bind(this));
}

delete(path: string): Observable<any> {
let url = this.getFullPath(path);
let headers = this.authentication.getHeaders();
this.status.next({ loading: true });
this.loading.start(`delete-${path}`);
return this.http.delete(url, { headers: headers }).map(res => {
this.status.next({ loading: false });
this.loading.finish(`delete-${path}`);
return res;
})
.catch(this.error.bind(this));
.catch(this.error.bind(this));
}

download(path: string): Observable<Blob | {}> {
let url = this.getFullPath(path);
let headers: HttpHeaders = this.authentication.getHeaders();
headers.set('Content-Type', 'application/x-www-form-urlencoded');
this.status.next({ loading: true });
headers = headers.set('Content-Type', 'application/x-www-form-urlencoded');
return this.http.get(url, {
responseType: 'blob',
headers: headers
}).map((blob: Blob) => {
this.status.next({ loading: false });
return blob;
})
.catch(this.error.bind(this));
.catch(this.error.bind(this));
}

getFullPath(path: string): string {
Expand All @@ -93,7 +98,7 @@ export class APIService {

private error(err: HttpErrorResponse) {
const error: Error = JSON.parse(err.error);
this.status.next({ loading: false, error: error });
this.loading.finish();
return Observable.throw(error);
}
}
2 changes: 2 additions & 0 deletions src/authentication.service.spec.ts
Expand Up @@ -7,6 +7,7 @@ import {
import { ConfigurationService } from './configuration.service';
import { AuthenticationService } from './authentication.service';
import { PasswordResetInfo } from './interfaces';
import { LoadingService } from './loading.service';

describe('AuthenticationService', () => {
beforeEach(() => {
Expand All @@ -15,6 +16,7 @@ describe('AuthenticationService', () => {
providers: [
AuthenticationService,
ConfigurationService,
LoadingService,
{
provide: 'CONFIGURATION', useValue: {
BACKEND_URL: 'http://fake/Plone',
Expand Down
30 changes: 26 additions & 4 deletions src/authentication.service.ts
@@ -1,11 +1,12 @@
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/Rx';

import { ConfigurationService } from './configuration.service';
import { AuthenticatedStatus, Error, PasswordResetInfo, UserInfo } from './interfaces';
import { Observable } from 'rxjs/Observable';
import { LoadingService } from './loading.service';


interface LoginToken {
Expand All @@ -19,6 +20,7 @@ export class AuthenticationService {
public isAuthenticated: BehaviorSubject<AuthenticatedStatus> = new BehaviorSubject({ state: false });

constructor(private config: ConfigurationService,
private loading: LoadingService,
private http: HttpClient,
@Inject(PLATFORM_ID) private platformId: Object) {
if (isPlatformBrowser(this.platformId)) {
Expand Down Expand Up @@ -57,10 +59,12 @@ export class AuthenticationService {
login: login,
password: password
});
this.loading.start('@login');
this.http.post(
this.config.get('BACKEND_URL') + '/@login', body, { headers: headers })
.subscribe(
(data: LoginToken) => {
this.loading.finish('@login');
if (data.token) {
localStorage.setItem('auth', data['token']);
localStorage.setItem('auth_time', (new Date()).toISOString());
Expand All @@ -72,6 +76,7 @@ export class AuthenticationService {
}
},
(httpErrorResponse: HttpErrorResponse) => {
this.loading.finish('@login');
localStorage.removeItem('auth');
localStorage.removeItem('auth_time');
const error: Error = JSON.parse(httpErrorResponse.error)['error'];
Expand All @@ -92,7 +97,12 @@ export class AuthenticationService {
requestPasswordReset(login: string): Observable<any> {
const headers = this.getHeaders();
const url = this.config.get('BACKEND_URL') + `/@users/${login}/reset-password`;
return this.http.post(url, {}, { headers: headers });
this.loading.start('request-password-reset');
return this.http.post(url, {}, { headers: headers }).map(res => {
this.loading.finish('request-password-reset');
return res;
})
.catch(this.error.bind(this));
}

passwordReset(resetInfo: PasswordResetInfo): Observable<any> {
Expand All @@ -107,7 +117,13 @@ export class AuthenticationService {
data['reset_token'] = resetInfo.token;
}
const url = this.config.get('BACKEND_URL') + `/@users/${resetInfo.login}/reset-password`;
return this.http.post(url, data, { headers: headers });
this.loading.start('reset-password');
return this.http.post(url, data, { headers: headers }).map(res => {
this.loading.finish('reset-password');
return res;
})
.catch(this.error.bind(this));
;
}

getHeaders(): HttpHeaders {
Expand All @@ -122,4 +138,10 @@ export class AuthenticationService {
}
return headers;
}

private error(err: HttpErrorResponse) {
const error: Error = JSON.parse(err.error);
this.loading.finish();
return Observable.throw(error);
}
}
10 changes: 7 additions & 3 deletions src/cache.service.spec.ts
Expand Up @@ -12,6 +12,7 @@ import { ConfigurationService } from './configuration.service';
import { AuthenticationService } from './authentication.service';
import { CacheService } from './cache.service';
import { Observable } from 'rxjs/Observable';
import { LoadingService } from './loading.service';

const front_page_response = {
"@id": "http://fake/Plone/",
Expand Down Expand Up @@ -51,8 +52,9 @@ describe('CacheService', () => {
imports: [HttpClientTestingModule],
providers: [
APIService,
ConfigurationService,
AuthenticationService,
ConfigurationService,
LoadingService,
{
provide: 'CONFIGURATION', useValue: {
BACKEND_URL: 'http://fake/Plone',
Expand Down Expand Up @@ -141,8 +143,9 @@ describe('CacheService', () => {
imports: [HttpClientTestingModule],
providers: [
APIService,
ConfigurationService,
AuthenticationService,
ConfigurationService,
LoadingService,
{
provide: 'CONFIGURATION', useValue: {
BACKEND_URL: 'http://fake/Plone',
Expand Down Expand Up @@ -184,8 +187,9 @@ describe('CacheService', () => {
imports: [HttpClientTestingModule],
providers: [
APIService,
ConfigurationService,
AuthenticationService,
ConfigurationService,
LoadingService,
{
provide: 'CONFIGURATION', useValue: {
BACKEND_URL: 'http://fake/Plone',
Expand Down
6 changes: 4 additions & 2 deletions src/comments.service.spec.ts
Expand Up @@ -15,6 +15,7 @@ import { AuthenticationService } from './authentication.service';
import { ConfigurationService } from './configuration.service';
import { CommentsService } from './comments.service';
import { CacheService } from './cache.service';
import { LoadingService } from './loading.service';

describe('CommentsService', () => {
beforeEach(() => {
Expand All @@ -23,11 +24,12 @@ describe('CommentsService', () => {
HttpClientTestingModule,
],
providers: [
CommentsService,
APIService,
CacheService,
AuthenticationService,
CacheService,
CommentsService,
ConfigurationService,
LoadingService,
{
provide: 'CONFIGURATION', useValue: {
BACKEND_URL: 'http://fake/Plone',
Expand Down
2 changes: 2 additions & 0 deletions src/components/breadcrumbs.spec.ts
Expand Up @@ -23,6 +23,7 @@ import { ResourceService } from '../resource.service';
import { Services } from '../services';
import { Breadcrumbs } from './breadcrumbs';
import { CacheService } from '../cache.service';
import { LoadingService } from '../loading.service';

@Injectable()
class MockResourceService {
Expand Down Expand Up @@ -66,6 +67,7 @@ describe('Breadcrumbs', () => {
},
CacheService,
CommentsService,
LoadingService,
NavigationService,
TypeMarker,
RESTAPIResolver,
Expand Down

0 comments on commit 3ebcfd8

Please sign in to comment.