Skip to content

Commit 5ad621b

Browse files
committed
fix issue where more than one api call was made on every route change
1 parent 404ccd9 commit 5ad621b

7 files changed

Lines changed: 174 additions & 71 deletions

File tree

src/app/core/data/request.service.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,4 +638,41 @@ describe('RequestService', () => {
638638
expect(done$).toBeObservable(cold('-----(t|)', { t: true }));
639639
}));
640640
});
641+
642+
describe('setStaleByHref', () => {
643+
const uuid = 'c574a42c-4818-47ac-bbe1-6c3cd622c81f';
644+
const href = 'https://rest.api/some/object';
645+
const freshRE: any = {
646+
request: { uuid, href },
647+
state: RequestEntryState.Success
648+
};
649+
const staleRE: any = {
650+
request: { uuid, href },
651+
state: RequestEntryState.SuccessStale
652+
};
653+
654+
it(`should call getByHref to retrieve the RequestEntry matching the href`, () => {
655+
spyOn(service, 'getByHref').and.returnValue(observableOf(staleRE));
656+
service.setStaleByHref(href);
657+
expect(service.getByHref).toHaveBeenCalledWith(href);
658+
});
659+
660+
it(`should dispatch a RequestStaleAction for the RequestEntry returned by getByHref`, (done: DoneFn) => {
661+
spyOn(service, 'getByHref').and.returnValue(observableOf(staleRE));
662+
spyOn(store, 'dispatch');
663+
service.setStaleByHref(href).subscribe(() => {
664+
expect(store.dispatch).toHaveBeenCalledWith(new RequestStaleAction(uuid));
665+
done();
666+
});
667+
});
668+
669+
it(`should emit true when the request in the store is stale`, () => {
670+
spyOn(service, 'getByHref').and.returnValue(cold('a-b', {
671+
a: freshRE,
672+
b: staleRE
673+
}));
674+
const result$ = service.setStaleByHref(href);
675+
expect(result$).toBeObservable(cold('--(c|)', { c: true }));
676+
});
677+
});
641678
});

src/app/core/data/request.service.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
RequestExecuteAction,
1717
RequestStaleAction
1818
} from './request.actions';
19-
import { GetRequest} from './request.models';
19+
import { GetRequest } from './request.models';
2020
import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
2121
import { RestRequestMethod } from './rest-request-method';
2222
import { coreSelector } from '../core.selectors';
@@ -331,7 +331,29 @@ export class RequestService {
331331
map((request: RequestEntry) => isStale(request.state)),
332332
filter((stale: boolean) => stale),
333333
take(1),
334-
);
334+
);
335+
}
336+
337+
/**
338+
* Mark a request as stale
339+
* @param href the href of the request
340+
* @return an Observable that will emit true once the Request becomes stale
341+
*/
342+
setStaleByHref(href: string): Observable<boolean> {
343+
const requestEntry$ = this.getByHref(href);
344+
345+
requestEntry$.pipe(
346+
map((re: RequestEntry) => re.request.uuid),
347+
take(1),
348+
).subscribe((uuid: string) => {
349+
this.store.dispatch(new RequestStaleAction(uuid));
350+
});
351+
352+
return requestEntry$.pipe(
353+
map((request: RequestEntry) => isStale(request.state)),
354+
filter((stale: boolean) => stale),
355+
take(1)
356+
);
335357
}
336358

337359
/**
@@ -344,10 +366,10 @@ export class RequestService {
344366
// if it's not a GET request
345367
if (request.method !== RestRequestMethod.GET) {
346368
return true;
347-
// if it is a GET request, check it isn't pending
369+
// if it is a GET request, check it isn't pending
348370
} else if (this.isPending(request)) {
349371
return false;
350-
// if it is pending, check if we're allowed to use a cached version
372+
// if it is pending, check if we're allowed to use a cached version
351373
} else if (!useCachedVersionIfAvailable) {
352374
return true;
353375
} else {

src/app/core/data/root-data.service.spec.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { RootDataService } from './root-data.service';
22
import { HALEndpointService } from '../shared/hal-endpoint.service';
3-
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
4-
import { Observable, of } from 'rxjs';
3+
import {
4+
createSuccessfulRemoteDataObject$,
5+
createFailedRemoteDataObject$
6+
} from '../../shared/remote-data.utils';
7+
import { Observable } from 'rxjs';
58
import { RemoteData } from './remote-data';
69
import { Root } from './root.model';
7-
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
810
import { cold } from 'jasmine-marbles';
911

1012
describe('RootDataService', () => {
1113
let service: RootDataService;
1214
let halService: HALEndpointService;
13-
let restService;
15+
let requestService;
1416
let rootEndpoint;
1517
let findByHrefSpy;
1618

@@ -19,10 +21,10 @@ describe('RootDataService', () => {
1921
halService = jasmine.createSpyObj('halService', {
2022
getRootHref: rootEndpoint,
2123
});
22-
restService = jasmine.createSpyObj('halService', {
23-
get: jasmine.createSpy('get'),
24-
});
25-
service = new RootDataService(null, null, null, halService, restService);
24+
requestService = jasmine.createSpyObj('requestService', [
25+
'setStaleByHref',
26+
]);
27+
service = new RootDataService(requestService, null, null, halService);
2628

2729
findByHrefSpy = spyOn(service as any, 'findByHref');
2830
findByHrefSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
@@ -47,12 +49,8 @@ describe('RootDataService', () => {
4749
let result$: Observable<boolean>;
4850

4951
it('should return observable of true when root endpoint is available', () => {
50-
const mockResponse = {
51-
statusCode: 200,
52-
statusText: 'OK'
53-
} as RawRestResponse;
52+
spyOn(service, 'findRoot').and.returnValue(createSuccessfulRemoteDataObject$<Root>({} as any));
5453

55-
restService.get.and.returnValue(of(mockResponse));
5654
result$ = service.checkServerAvailability();
5755

5856
expect(result$).toBeObservable(cold('(a|)', {
@@ -61,12 +59,8 @@ describe('RootDataService', () => {
6159
});
6260

6361
it('should return observable of false when root endpoint is not available', () => {
64-
const mockResponse = {
65-
statusCode: 500,
66-
statusText: 'Internal Server Error'
67-
} as RawRestResponse;
62+
spyOn(service, 'findRoot').and.returnValue(createFailedRemoteDataObject$<Root>('500'));
6863

69-
restService.get.and.returnValue(of(mockResponse));
7064
result$ = service.checkServerAvailability();
7165

7266
expect(result$).toBeObservable(cold('(a|)', {
@@ -75,4 +69,12 @@ describe('RootDataService', () => {
7569
});
7670

7771
});
72+
73+
describe(`invalidateRootCache`, () => {
74+
it(`should set the cached root request to stale`, () => {
75+
service.invalidateRootCache();
76+
expect(halService.getRootHref).toHaveBeenCalled();
77+
expect(requestService.setStaleByHref).toHaveBeenCalledWith(rootEndpoint);
78+
});
79+
});
7880
});

src/app/core/data/root-data.service.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
77
import { Observable, of as observableOf } from 'rxjs';
88
import { RemoteData } from './remote-data';
99
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
10-
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
11-
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
1210
import { catchError, map } from 'rxjs/operators';
1311
import { BaseDataService } from './base/base-data.service';
1412
import { ObjectCacheService } from '../cache/object-cache.service';
1513
import { dataService } from './base/data-service.decorator';
14+
import { getFirstCompletedRemoteData } from '../shared/operators';
1615

1716
/**
1817
* A service to retrieve the {@link Root} object from the REST API.
@@ -25,21 +24,21 @@ export class RootDataService extends BaseDataService<Root> {
2524
protected rdbService: RemoteDataBuildService,
2625
protected objectCache: ObjectCacheService,
2726
protected halService: HALEndpointService,
28-
protected restService: DspaceRestService,
2927
) {
30-
super('', requestService, rdbService, objectCache, halService, 6 * 60 * 60 * 1000);
28+
super('', requestService, rdbService, objectCache, halService, 60 * 1000);
3129
}
3230

3331
/**
3432
* Check if root endpoint is available
3533
*/
3634
checkServerAvailability(): Observable<boolean> {
37-
return this.restService.get(this.halService.getRootHref()).pipe(
35+
return this.findRoot().pipe(
3836
catchError((err ) => {
3937
console.error(err);
4038
return observableOf(false);
4139
}),
42-
map((res: RawRestResponse) => res.statusCode === 200)
40+
getFirstCompletedRemoteData(),
41+
map((rootRd: RemoteData<Root>) => rootRd.statusCode === 200)
4342
);
4443
}
4544

@@ -60,6 +59,6 @@ export class RootDataService extends BaseDataService<Root> {
6059
* Set to sale the root endpoint cache hit
6160
*/
6261
invalidateRootCache() {
63-
this.requestService.setStaleByHrefSubstring(this.halService.getRootHref());
62+
this.requestService.setStaleByHref(this.halService.getRootHref());
6463
}
6564
}
Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,78 @@
11
import { ServerCheckGuard } from './server-check.guard';
2-
import { Router } from '@angular/router';
2+
import { Router, NavigationStart, UrlTree, NavigationEnd, RouterEvent } from '@angular/router';
33

4-
import { of } from 'rxjs';
5-
import { take } from 'rxjs/operators';
6-
7-
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
4+
import { of, ReplaySubject } from 'rxjs';
85
import { RootDataService } from '../data/root-data.service';
6+
import { TestScheduler } from 'rxjs/testing';
97
import SpyObj = jasmine.SpyObj;
108

119
describe('ServerCheckGuard', () => {
1210
let guard: ServerCheckGuard;
13-
let router: SpyObj<Router>;
11+
let router: Router;
12+
const eventSubject = new ReplaySubject<RouterEvent>(1);
1413
let rootDataServiceStub: SpyObj<RootDataService>;
15-
16-
rootDataServiceStub = jasmine.createSpyObj('RootDataService', {
17-
checkServerAvailability: jasmine.createSpy('checkServerAvailability'),
18-
invalidateRootCache: jasmine.createSpy('invalidateRootCache')
19-
});
20-
router = jasmine.createSpyObj('Router', {
21-
navigateByUrl: jasmine.createSpy('navigateByUrl')
22-
});
14+
let testScheduler: TestScheduler;
15+
let redirectUrlTree: UrlTree;
2316

2417
beforeEach(() => {
18+
testScheduler = new TestScheduler((actual, expected) => {
19+
expect(actual).toEqual(expected);
20+
});
21+
rootDataServiceStub = jasmine.createSpyObj('RootDataService', {
22+
checkServerAvailability: jasmine.createSpy('checkServerAvailability'),
23+
invalidateRootCache: jasmine.createSpy('invalidateRootCache')
24+
});
25+
redirectUrlTree = new UrlTree();
26+
router = {
27+
events: eventSubject.asObservable(),
28+
navigateByUrl: jasmine.createSpy('navigateByUrl'),
29+
parseUrl: jasmine.createSpy('parseUrl').and.returnValue(redirectUrlTree)
30+
} as any;
2531
guard = new ServerCheckGuard(router, rootDataServiceStub);
2632
});
2733

28-
afterEach(() => {
29-
router.navigateByUrl.calls.reset();
30-
rootDataServiceStub.invalidateRootCache.calls.reset();
31-
});
32-
3334
it('should be created', () => {
3435
expect(guard).toBeTruthy();
3536
});
3637

37-
describe('when root endpoint has succeeded', () => {
38+
describe('when root endpoint request has succeeded', () => {
3839
beforeEach(() => {
3940
rootDataServiceStub.checkServerAvailability.and.returnValue(of(true));
4041
});
4142

42-
it('should not redirect to error page', () => {
43-
guard.canActivateChild({} as any, {} as any).pipe(
44-
take(1)
45-
).subscribe((canActivate: boolean) => {
46-
expect(canActivate).toEqual(true);
47-
expect(rootDataServiceStub.invalidateRootCache).not.toHaveBeenCalled();
48-
expect(router.navigateByUrl).not.toHaveBeenCalled();
43+
it('should return true', () => {
44+
testScheduler.run(({ expectObservable }) => {
45+
const result$ = guard.canActivateChild({} as any, {} as any);
46+
expectObservable(result$).toBe('(a|)', { a: true });
4947
});
5048
});
5149
});
5250

53-
describe('when root endpoint has not succeeded', () => {
51+
describe('when root endpoint request has not succeeded', () => {
5452
beforeEach(() => {
5553
rootDataServiceStub.checkServerAvailability.and.returnValue(of(false));
5654
});
5755

58-
it('should redirect to error page', () => {
59-
guard.canActivateChild({} as any, {} as any).pipe(
60-
take(1)
61-
).subscribe((canActivate: boolean) => {
62-
expect(canActivate).toEqual(false);
63-
expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalled();
64-
expect(router.navigateByUrl).toHaveBeenCalledWith(getPageInternalServerErrorRoute());
56+
it('should return a UrlTree with the route to the 500 error page', () => {
57+
testScheduler.run(({ expectObservable }) => {
58+
const result$ = guard.canActivateChild({} as any, {} as any);
59+
expectObservable(result$).toBe('(b|)', { b: redirectUrlTree });
6560
});
61+
expect(router.parseUrl).toHaveBeenCalledWith('/500');
62+
});
63+
});
64+
65+
describe(`listenForRouteChanges`, () => {
66+
it(`should invalidate the root cache on every NavigationStart event`, () => {
67+
testScheduler.run(() => {
68+
guard.listenForRouteChanges();
69+
eventSubject.next(new NavigationStart(1,''));
70+
eventSubject.next(new NavigationEnd(1,'', ''));
71+
eventSubject.next(new NavigationStart(2,''));
72+
eventSubject.next(new NavigationEnd(2,'', ''));
73+
eventSubject.next(new NavigationStart(3,''));
74+
});
75+
expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalledTimes(3);
6676
});
6777
});
6878
});

src/app/core/server-check/server-check.guard.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import { Injectable } from '@angular/core';
2-
import { ActivatedRouteSnapshot, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router';
2+
import {
3+
ActivatedRouteSnapshot,
4+
CanActivateChild,
5+
Router,
6+
RouterStateSnapshot,
7+
UrlTree,
8+
NavigationStart
9+
} from '@angular/router';
310

411
import { Observable } from 'rxjs';
5-
import { take, tap } from 'rxjs/operators';
12+
import { take, map, filter } from 'rxjs/operators';
613

714
import { RootDataService } from '../data/root-data.service';
815
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
@@ -23,17 +30,32 @@ export class ServerCheckGuard implements CanActivateChild {
2330
*/
2431
canActivateChild(
2532
route: ActivatedRouteSnapshot,
26-
state: RouterStateSnapshot): Observable<boolean> {
33+
state: RouterStateSnapshot
34+
): Observable<boolean | UrlTree> {
2735

2836
return this.rootDataService.checkServerAvailability().pipe(
2937
take(1),
30-
tap((isAvailable: boolean) => {
38+
map((isAvailable: boolean) => {
3139
if (!isAvailable) {
32-
this.rootDataService.invalidateRootCache();
33-
this.router.navigateByUrl(getPageInternalServerErrorRoute());
40+
return this.router.parseUrl(getPageInternalServerErrorRoute());
41+
} else {
42+
return true;
3443
}
3544
})
3645
);
46+
}
3747

48+
/**
49+
* Listen to all router events. Every time a new navigation starts, invalidate the cache
50+
* for the root endpoint. That way we retrieve it once per routing operation to ensure the
51+
* backend is not down. But if the guard is called multiple times during the same routing
52+
* operation, the cached version is used.
53+
*/
54+
listenForRouteChanges(): void {
55+
this.router.events.pipe(
56+
filter(event => event instanceof NavigationStart),
57+
).subscribe(() => {
58+
this.rootDataService.invalidateRootCache();
59+
});
3860
}
3961
}

0 commit comments

Comments
 (0)