Skip to content

Commit f8404c5

Browse files
committed
94207: Extract 'wait for invalidation' pattern into a new method
1 parent 030b1c3 commit f8404c5

10 files changed

Lines changed: 217 additions & 288 deletions

src/app/core/cache/builders/remote-data-build.service.spec.ts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
1+
import { createFailedRemoteDataObject, createPendingRemoteDataObject, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
22
import { buildPaginatedList, PaginatedList } from '../../data/paginated-list.model';
33
import { Item } from '../../shared/item.model';
44
import { PageInfo } from '../../shared/page-info.model';
@@ -19,6 +19,8 @@ import { HALLink } from '../../shared/hal-link.model';
1919
import { RequestEntryState } from '../../data/request-entry-state.model';
2020
import { RequestEntry } from '../../data/request-entry.model';
2121
import { cold } from 'jasmine-marbles';
22+
import { TestScheduler } from 'rxjs/testing';
23+
import { fakeAsync, tick } from '@angular/core/testing';
2224

2325
describe('RemoteDataBuildService', () => {
2426
let service: RemoteDataBuildService;
@@ -763,4 +765,95 @@ describe('RemoteDataBuildService', () => {
763765
});
764766
});
765767
});
768+
769+
describe('buildFromRequestUUIDAndAwait', () => {
770+
let testScheduler;
771+
772+
let callback: jasmine.Spy;
773+
let buildFromRequestUUIDSpy;
774+
775+
const BOOLEAN = { t: true, f: false };
776+
777+
const MOCK_PENDING_RD = createPendingRemoteDataObject();
778+
const MOCK_SUCCEEDED_RD = createSuccessfulRemoteDataObject({});
779+
const MOCK_FAILED_RD = createFailedRemoteDataObject('failed');
780+
781+
const RDs = {
782+
p: MOCK_PENDING_RD,
783+
s: MOCK_SUCCEEDED_RD,
784+
f: MOCK_FAILED_RD,
785+
};
786+
787+
788+
beforeEach(() => {
789+
testScheduler = new TestScheduler((actual, expected) => {
790+
expect(actual).toEqual(expected);
791+
});
792+
793+
callback = jasmine.createSpy('callback');
794+
callback.and.returnValue(observableOf(undefined));
795+
buildFromRequestUUIDSpy = spyOn(service, 'buildFromRequestUUID').and.callThrough();
796+
});
797+
798+
it('should patch through href & followLinks to buildFromRequestUUID', () => {
799+
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
800+
service.buildFromRequestUUIDAndAwait('some-href', callback, ...linksToFollow);
801+
expect(buildFromRequestUUIDSpy).toHaveBeenCalledWith('some-href', ...linksToFollow);
802+
});
803+
804+
it('should trigger the callback on successful RD', (done) => {
805+
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
806+
807+
service.buildFromRequestUUIDAndAwait('some-href', callback).subscribe(rd => {
808+
expect(rd).toBe(MOCK_SUCCEEDED_RD);
809+
expect(callback).toHaveBeenCalled();
810+
done();
811+
});
812+
});
813+
814+
it('should trigger the callback on successful RD even if nothing subscribes to the returned Observable', fakeAsync(() => {
815+
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
816+
817+
service.buildFromRequestUUIDAndAwait('some-href', callback);
818+
tick();
819+
820+
expect(callback).toHaveBeenCalled();
821+
}));
822+
823+
it('should not trigger the callback on pending RD', (done) => {
824+
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_PENDING_RD));
825+
826+
service.buildFromRequestUUIDAndAwait('some-href', callback).subscribe(rd => {
827+
expect(rd).toBe(MOCK_PENDING_RD);
828+
expect(callback).not.toHaveBeenCalled();
829+
done();
830+
});
831+
});
832+
833+
it('should not trigger the callback on failed RD', (done) => {
834+
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_FAILED_RD));
835+
836+
service.buildFromRequestUUIDAndAwait('some-href', callback).subscribe(rd => {
837+
expect(rd).toBe(MOCK_FAILED_RD);
838+
expect(callback).not.toHaveBeenCalled();
839+
done();
840+
});
841+
});
842+
843+
it('should only emit after the callback is done', () => {
844+
testScheduler.run(({ cold: tsCold, expectObservable }) => {
845+
buildFromRequestUUIDSpy.and.returnValue(
846+
tsCold('-p----s', RDs)
847+
);
848+
callback.and.returnValue(
849+
tsCold(' --t', BOOLEAN)
850+
);
851+
852+
const done$ = service.buildFromRequestUUIDAndAwait('some-href', callback);
853+
expectObservable(done$).toBe(
854+
' -p------s', RDs // resulting duration between pending & successful includes the callback
855+
);
856+
});
857+
});
858+
});
766859
});

src/app/core/cache/builders/remote-data-build.service.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Injectable } from '@angular/core';
22
import {
3+
AsyncSubject,
34
combineLatest as observableCombineLatest,
45
Observable,
56
of as observableOf,
@@ -24,6 +25,7 @@ import { hasSucceeded, isStale, RequestEntryState } from '../../data/request-ent
2425
import { getRequestFromRequestHref, getRequestFromRequestUUID } from '../../shared/request.operators';
2526
import { RequestEntry } from '../../data/request-entry.model';
2627
import { ResponseState } from '../../data/response-state.model';
28+
import { getFirstCompletedRemoteData } from '../../shared/operators';
2729

2830
@Injectable()
2931
export class RemoteDataBuildService {
@@ -188,6 +190,49 @@ export class RemoteDataBuildService {
188190
return this.toRemoteDataObservable<T>(requestEntry$, payload$);
189191
}
190192

193+
/**
194+
* Creates a {@link RemoteData} object for a rest request and its response
195+
* and emits it only after the callback function is completed.
196+
*
197+
* @param requestUUID$ The UUID of the request we want to retrieve
198+
* @param callback A function that returns an Observable. It will only be called once the request has succeeded.
199+
* Then, the response will only be emitted after this callback function has emitted.
200+
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
201+
*/
202+
buildFromRequestUUIDAndAwait<T>(requestUUID$: string | Observable<string>, callback: (rd?: RemoteData<T>) => Observable<unknown>, ...linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<T>> {
203+
const response$ = this.buildFromRequestUUID(requestUUID$, ...linksToFollow);
204+
205+
const callbackDone$ = new AsyncSubject<boolean>();
206+
response$.pipe(
207+
getFirstCompletedRemoteData(),
208+
switchMap((rd: RemoteData<any>) => {
209+
if (rd.hasSucceeded) {
210+
// if the request succeeded, execute the callback
211+
return callback(rd);
212+
} else {
213+
// otherwise, emit right away so the subscription doesn't stick around
214+
return [true];
215+
}
216+
}),
217+
).subscribe(() => {
218+
callbackDone$.next(true);
219+
callbackDone$.complete();
220+
});
221+
222+
return response$.pipe(
223+
switchMap((rd: RemoteData<any>) => {
224+
if (rd.hasSucceeded) {
225+
// if the request succeeded, wait for the callback to finish
226+
return callbackDone$.pipe(
227+
map(() => rd),
228+
);
229+
} else {
230+
return [rd];
231+
}
232+
})
233+
);
234+
}
235+
191236
/**
192237
* Creates a {@link RemoteData} object for a rest request and its response
193238
*

src/app/core/data/bitstream-format-data.service.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ describe('BitstreamFormatDataService', () => {
5959
function initTestService(halService) {
6060
rd = createSuccessfulRemoteDataObject({});
6161
rdbService = jasmine.createSpyObj('rdbService', {
62-
buildFromRequestUUID: observableOf(rd)
62+
buildFromRequestUUID: observableOf(rd),
63+
buildFromRequestUUIDAndAwait: observableOf(rd),
6364
});
6465

6566
return new BitstreamFormatDataService(

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

Lines changed: 20 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -920,24 +920,24 @@ describe('DataService', () => {
920920
let MOCK_SUCCEEDED_RD;
921921
let MOCK_FAILED_RD;
922922

923-
let invalidateByHrefSpy: jasmine.Spy;
924-
let buildFromRequestUUIDSpy: jasmine.Spy;
923+
let buildFromRequestUUIDAndAwaitSpy: jasmine.Spy;
925924
let getIDHrefObsSpy: jasmine.Spy;
926925
let deleteByHrefSpy: jasmine.Spy;
926+
let invalidateByHrefSpy: jasmine.Spy;
927927

928928
beforeEach(() => {
929-
invalidateByHrefSpy = spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true));
930-
buildFromRequestUUIDSpy = spyOn(rdbService, 'buildFromRequestUUID').and.callThrough();
929+
buildFromRequestUUIDAndAwaitSpy = spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callThrough();
931930
getIDHrefObsSpy = spyOn(service, 'getIDHrefObs').and.callThrough();
932931
deleteByHrefSpy = spyOn(service, 'deleteByHref').and.callThrough();
932+
invalidateByHrefSpy = spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true));
933933

934934
MOCK_SUCCEEDED_RD = createSuccessfulRemoteDataObject({});
935935
MOCK_FAILED_RD = createFailedRemoteDataObject('something went wrong');
936936
});
937937

938938
it('should retrieve href by ID and call deleteByHref', () => {
939939
getIDHrefObsSpy.and.returnValue(observableOf('some-href'));
940-
buildFromRequestUUIDSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
940+
buildFromRequestUUIDAndAwaitSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
941941

942942
service.delete('some-id', ['a', 'b', 'c']).subscribe(rd => {
943943
expect(getIDHrefObsSpy).toHaveBeenCalledWith('some-id');
@@ -946,65 +946,27 @@ describe('DataService', () => {
946946
});
947947

948948
describe('deleteByHref', () => {
949-
it('should call invalidateByHref if the DELETE request succeeds', (done) => {
950-
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
951-
952-
service.deleteByHref('some-href').subscribe(rd => {
953-
expect(rd).toBe(MOCK_SUCCEEDED_RD);
954-
expect(invalidateByHrefSpy).toHaveBeenCalled();
955-
done();
956-
});
949+
beforeEach(() => {
950+
buildFromRequestUUIDAndAwaitSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
957951
});
958952

959-
it('should call invalidateByHref even if not subscribing to returned Observable', fakeAsync(() => {
960-
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
961-
962-
service.deleteByHref('some-href');
963-
tick();
964-
965-
expect(invalidateByHrefSpy).toHaveBeenCalled();
966-
}));
967-
968-
it('should not call invalidateByHref if the DELETE request fails', (done) => {
969-
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_FAILED_RD));
953+
it('should send a DELETE request', () => {
954+
service.deleteByHref('https://somewhere.org/something/123', ['things', 'stuff']);
970955

971-
service.deleteByHref('some-href').subscribe(rd => {
972-
expect(rd).toBe(MOCK_FAILED_RD);
973-
expect(invalidateByHrefSpy).not.toHaveBeenCalled();
974-
done();
975-
});
956+
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
957+
method: 'DELETE',
958+
href: 'https://somewhere.org/something/123?copyVirtualMetadata=things&copyVirtualMetadata=stuff'
959+
}));
976960
});
977961

978-
it('should wait for invalidateByHref before emitting', () => {
979-
testScheduler.run(({ cold, expectObservable }) => {
980-
buildFromRequestUUIDSpy.and.returnValue(
981-
cold('(r|)', { r: MOCK_SUCCEEDED_RD}) // RD emits right away
982-
);
983-
invalidateByHrefSpy.and.returnValue(
984-
cold('----(t|)', BOOLEAN) // but we pretend that setting requests to stale takes longer
985-
);
986-
987-
const done$ = service.deleteByHref('some-href');
988-
expectObservable(done$).toBe(
989-
'----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait until that's done
990-
);
991-
});
992-
});
962+
it('should retrieve response via buildFromRequestUUIDAndAwait & invalidate within the callback', () => {
963+
service.deleteByHref('https://somewhere.org/something/123');
993964

994-
it('should wait for the DELETE request to resolve before emitting', () => {
995-
testScheduler.run(({ cold, expectObservable }) => {
996-
buildFromRequestUUIDSpy.and.returnValue(
997-
cold('----(r|)', { r: MOCK_SUCCEEDED_RD}) // the request takes a while
998-
);
999-
invalidateByHrefSpy.and.returnValue(
1000-
cold('(t|)', BOOLEAN) // but we pretend that setting to stale happens sooner
1001-
); // e.g.: maybe already stale before this call?
1002-
1003-
const done$ = service.deleteByHref('some-href');
1004-
expectObservable(done$).toBe(
1005-
'----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait for the request
1006-
);
1007-
});
965+
expect(buildFromRequestUUIDAndAwaitSpy).toHaveBeenCalled();
966+
expect(buildFromRequestUUIDAndAwaitSpy.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
967+
const callback = buildFromRequestUUIDAndAwaitSpy.calls.argsFor(0)[1];
968+
callback();
969+
expect(invalidateByHrefSpy).toHaveBeenCalledWith('https://somewhere.org/something/123');
1008970
});
1009971
});
1010972
});

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

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -649,35 +649,7 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
649649
}
650650
this.requestService.send(request);
651651

652-
const response$ = this.rdbService.buildFromRequestUUID(requestId);
653-
654-
const invalidated$ = new AsyncSubject<boolean>();
655-
response$.pipe(
656-
getFirstCompletedRemoteData(),
657-
switchMap((rd: RemoteData<NoContent>) => {
658-
if (rd.hasSucceeded) {
659-
return this.invalidateByHref(href);
660-
} else {
661-
return [true];
662-
}
663-
})
664-
).subscribe(() => {
665-
invalidated$.next(true);
666-
invalidated$.complete();
667-
});
668-
669-
return response$.pipe(
670-
switchMap((rd: RemoteData<NoContent>) => {
671-
if (rd.hasSucceeded) {
672-
return invalidated$.pipe(
673-
filter((invalidated: boolean) => invalidated),
674-
map(() => rd)
675-
);
676-
} else {
677-
return [rd];
678-
}
679-
})
680-
);
652+
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => this.invalidateByHref(href));
681653
}
682654

683655
/**

src/app/core/eperson/group-data.service.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ describe('GroupDataService', () => {
9595
store = new Store<CoreState>(undefined, undefined, undefined);
9696
service = initTestService();
9797
spyOn(store, 'dispatch');
98+
spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callThrough();
9899
});
99100

100101
describe('searchGroups', () => {
@@ -136,6 +137,11 @@ describe('GroupDataService', () => {
136137
expect(requestService.send).toHaveBeenCalledWith(expected);
137138
});
138139
it('should invalidate the previous requests of the parent group', () => {
140+
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
141+
expect(rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
142+
const callback = rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[1];
143+
callback();
144+
139145
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
140146
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(2);
141147
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
@@ -156,6 +162,11 @@ describe('GroupDataService', () => {
156162
expect(requestService.send).toHaveBeenCalledWith(expected);
157163
});
158164
it('should invalidate the previous requests of the parent group\'', () => {
165+
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
166+
expect(rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
167+
const callback = rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[1];
168+
callback();
169+
159170
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
160171
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(2);
161172
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
@@ -180,6 +191,11 @@ describe('GroupDataService', () => {
180191
expect(requestService.send).toHaveBeenCalledWith(expected);
181192
});
182193
it('should invalidate the previous requests of the EPerson and the group', () => {
194+
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
195+
expect(rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
196+
const callback = rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[1];
197+
callback();
198+
183199
expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock2._links.self.href);
184200
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
185201
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(4);
@@ -201,6 +217,11 @@ describe('GroupDataService', () => {
201217
expect(requestService.send).toHaveBeenCalledWith(expected);
202218
});
203219
it('should invalidate the previous requests of the EPerson and the group', () => {
220+
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
221+
expect(rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
222+
const callback = rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[1];
223+
callback();
224+
204225
expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock._links.self.href);
205226
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
206227
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(4);

0 commit comments

Comments
 (0)