Skip to content

Commit 8045e4c

Browse files
authored
Merge pull request DSpace#2529 from atmire/fix-mutliple-api-calls-on-route-change-8.0.0-next
[Port main] Fix multiple api calls on route change
2 parents 4491e6c + 623fc6e commit 8045e4c

7 files changed

Lines changed: 187 additions & 70 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
@@ -639,6 +639,43 @@ describe('RequestService', () => {
639639
}));
640640
});
641641

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+
});
678+
642679
describe('setStaleByHrefSubstring', () => {
643680
let dispatchSpy: jasmine.Spy;
644681
let getByUUIDSpy: jasmine.Spy;

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';
@@ -351,7 +351,29 @@ export class RequestService {
351351
map((request: RequestEntry) => isStale(request.state)),
352352
filter((stale: boolean) => stale),
353353
take(1),
354-
);
354+
);
355+
}
356+
357+
/**
358+
* Mark a request as stale
359+
* @param href the href of the request
360+
* @return an Observable that will emit true once the Request becomes stale
361+
*/
362+
setStaleByHref(href: string): Observable<boolean> {
363+
const requestEntry$ = this.getByHref(href);
364+
365+
requestEntry$.pipe(
366+
map((re: RequestEntry) => re.request.uuid),
367+
take(1),
368+
).subscribe((uuid: string) => {
369+
this.store.dispatch(new RequestStaleAction(uuid));
370+
});
371+
372+
return requestEntry$.pipe(
373+
map((request: RequestEntry) => isStale(request.state)),
374+
filter((stale: boolean) => stale),
375+
take(1)
376+
);
355377
}
356378

357379
/**
@@ -364,10 +386,10 @@ export class RequestService {
364386
// if it's not a GET request
365387
if (request.method !== RestRequestMethod.GET) {
366388
return true;
367-
// if it is a GET request, check it isn't pending
389+
// if it is a GET request, check it isn't pending
368390
} else if (this.isPending(request)) {
369391
return false;
370-
// if it is pending, check if we're allowed to use a cached version
392+
// if it is pending, check if we're allowed to use a cached version
371393
} else if (!useCachedVersionIfAvailable) {
372394
return true;
373395
} 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: 5 additions & 6 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,7 +24,6 @@ export class RootDataService extends BaseDataService<Root> {
2524
protected rdbService: RemoteDataBuildService,
2625
protected objectCache: ObjectCacheService,
2726
protected halService: HALEndpointService,
28-
protected restService: DspaceRestService,
2927
) {
3028
super('', requestService, rdbService, objectCache, halService, 6 * 60 * 60 * 1000);
3129
}
@@ -34,12 +32,13 @@ export class RootDataService extends BaseDataService<Root> {
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: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,86 @@
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+
findRoot: jasmine.createSpy('findRoot')
25+
});
26+
redirectUrlTree = new UrlTree();
27+
router = {
28+
events: eventSubject.asObservable(),
29+
navigateByUrl: jasmine.createSpy('navigateByUrl'),
30+
parseUrl: jasmine.createSpy('parseUrl').and.returnValue(redirectUrlTree)
31+
} as any;
2532
guard = new ServerCheckGuard(router, rootDataServiceStub);
2633
});
2734

28-
afterEach(() => {
29-
router.navigateByUrl.calls.reset();
30-
rootDataServiceStub.invalidateRootCache.calls.reset();
31-
});
32-
3335
it('should be created', () => {
3436
expect(guard).toBeTruthy();
3537
});
3638

37-
describe('when root endpoint has succeeded', () => {
39+
describe('when root endpoint request has succeeded', () => {
3840
beforeEach(() => {
3941
rootDataServiceStub.checkServerAvailability.and.returnValue(of(true));
4042
});
4143

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();
44+
it('should return true', () => {
45+
testScheduler.run(({ expectObservable }) => {
46+
const result$ = guard.canActivateChild({} as any, {} as any);
47+
expectObservable(result$).toBe('(a|)', { a: true });
4948
});
5049
});
5150
});
5251

53-
describe('when root endpoint has not succeeded', () => {
52+
describe('when root endpoint request has not succeeded', () => {
5453
beforeEach(() => {
5554
rootDataServiceStub.checkServerAvailability.and.returnValue(of(false));
5655
});
5756

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());
57+
it('should return a UrlTree with the route to the 500 error page', () => {
58+
testScheduler.run(({ expectObservable }) => {
59+
const result$ = guard.canActivateChild({} as any, {} as any);
60+
expectObservable(result$).toBe('(b|)', { b: redirectUrlTree });
6561
});
62+
expect(router.parseUrl).toHaveBeenCalledWith('/500');
63+
});
64+
});
65+
66+
describe(`listenForRouteChanges`, () => {
67+
it(`should retrieve the root endpoint, without using the cache, when the method is first called`, () => {
68+
testScheduler.run(() => {
69+
guard.listenForRouteChanges();
70+
expect(rootDataServiceStub.findRoot).toHaveBeenCalledWith(false);
71+
});
72+
});
73+
74+
it(`should invalidate the root cache on every NavigationStart event`, () => {
75+
testScheduler.run(() => {
76+
guard.listenForRouteChanges();
77+
eventSubject.next(new NavigationStart(1,''));
78+
eventSubject.next(new NavigationEnd(1,'', ''));
79+
eventSubject.next(new NavigationStart(2,''));
80+
eventSubject.next(new NavigationEnd(2,'', ''));
81+
eventSubject.next(new NavigationStart(3,''));
82+
});
83+
expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalledTimes(3);
6684
});
6785
});
6886
});

0 commit comments

Comments
 (0)