Skip to content

Commit edd193a

Browse files
authored
Merge pull request #1813 from atmire/w2p-94207_Fix-resource-policy-cache-issues
Fix ResourcePolicy cache issues
2 parents f0fd196 + 3717c66 commit edd193a

17 files changed

Lines changed: 664 additions & 501 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/base/delete-data.spec.ts

Lines changed: 34 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { RequestEntryState } from '../request-entry-state.model';
2222
import { DeleteData, DeleteDataImpl } from './delete-data';
2323
import { NotificationsService } from '../../../shared/notifications/notifications.service';
2424
import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
25-
import { fakeAsync, tick } from '@angular/core/testing';
25+
import { RestRequestMethod } from '../rest-request-method';
2626

2727
/**
2828
* Tests whether calls to `DeleteData` methods are correctly patched through in a concrete data service that implements it
@@ -63,8 +63,6 @@ export function testDeleteDataImplementation(serviceFactory: () => DeleteData<an
6363

6464
const endpoint = 'https://rest.api/core';
6565

66-
const BOOLEAN = { f: false, t: true };
67-
6866
class TestService extends DeleteDataImpl<any> {
6967
constructor(
7068
protected requestService: RequestService,
@@ -155,13 +153,13 @@ describe('DeleteDataImpl', () => {
155153
let MOCK_FAILED_RD;
156154

157155
let invalidateByHrefSpy: jasmine.Spy;
158-
let buildFromRequestUUIDSpy: jasmine.Spy;
156+
let buildFromRequestUUIDAndAwaitSpy: jasmine.Spy;
159157
let getIDHrefObsSpy: jasmine.Spy;
160158
let deleteByHrefSpy: jasmine.Spy;
161159

162160
beforeEach(() => {
163161
invalidateByHrefSpy = spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true));
164-
buildFromRequestUUIDSpy = spyOn(rdbService, 'buildFromRequestUUID').and.callThrough();
162+
buildFromRequestUUIDAndAwaitSpy = spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callThrough();
165163
getIDHrefObsSpy = spyOn(service, 'getIDHrefObs').and.callThrough();
166164
deleteByHrefSpy = spyOn(service, 'deleteByHref').and.callThrough();
167165

@@ -171,7 +169,7 @@ describe('DeleteDataImpl', () => {
171169

172170
it('should retrieve href by ID and call deleteByHref', () => {
173171
getIDHrefObsSpy.and.returnValue(observableOf('some-href'));
174-
buildFromRequestUUIDSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
172+
buildFromRequestUUIDAndAwaitSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
175173

176174
service.delete('some-id', ['a', 'b', 'c']).subscribe(rd => {
177175
expect(getIDHrefObsSpy).toHaveBeenCalledWith('some-id');
@@ -180,64 +178,51 @@ describe('DeleteDataImpl', () => {
180178
});
181179

182180
describe('deleteByHref', () => {
183-
it('should call invalidateByHref if the DELETE request succeeds', (done) => {
184-
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
185-
186-
service.deleteByHref('some-href').subscribe(rd => {
187-
expect(rd).toBe(MOCK_SUCCEEDED_RD);
188-
expect(invalidateByHrefSpy).toHaveBeenCalled();
181+
it('should send a DELETE request', (done) => {
182+
buildFromRequestUUIDAndAwaitSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
183+
184+
service.deleteByHref('some-href').subscribe(() => {
185+
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
186+
method: RestRequestMethod.DELETE,
187+
href: 'some-href',
188+
}));
189189
done();
190190
});
191191
});
192192

193-
it('should call invalidateByHref even if not subscribing to returned Observable', fakeAsync(() => {
194-
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
195-
196-
service.deleteByHref('some-href');
197-
tick();
193+
it('should include the virtual metadata to be copied in the DELETE request', (done) => {
194+
buildFromRequestUUIDAndAwaitSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
198195

199-
expect(invalidateByHrefSpy).toHaveBeenCalled();
200-
}));
201-
202-
it('should not call invalidateByHref if the DELETE request fails', (done) => {
203-
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_FAILED_RD));
204-
205-
service.deleteByHref('some-href').subscribe(rd => {
206-
expect(rd).toBe(MOCK_FAILED_RD);
207-
expect(invalidateByHrefSpy).not.toHaveBeenCalled();
196+
service.deleteByHref('some-href', ['a', 'b', 'c']).subscribe(() => {
197+
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
198+
method: RestRequestMethod.DELETE,
199+
href: 'some-href?copyVirtualMetadata=a&copyVirtualMetadata=b&copyVirtualMetadata=c',
200+
}));
208201
done();
209202
});
210203
});
211204

212-
it('should wait for invalidateByHref before emitting', () => {
213-
testScheduler.run(({ cold, expectObservable }) => {
214-
buildFromRequestUUIDSpy.and.returnValue(
215-
cold('(r|)', { r: MOCK_SUCCEEDED_RD}) // RD emits right away
216-
);
217-
invalidateByHrefSpy.and.returnValue(
218-
cold('----(t|)', BOOLEAN) // but we pretend that setting requests to stale takes longer
205+
it('should invalidate the currently cached object', (done) => {
206+
service.deleteByHref('some-href').subscribe(() => {
207+
expect(buildFromRequestUUIDAndAwaitSpy).toHaveBeenCalledWith(
208+
requestService.generateRequestId(),
209+
jasmine.anything(),
219210
);
220211

221-
const done$ = service.deleteByHref('some-href');
222-
expectObservable(done$).toBe(
223-
'----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait until that's done
224-
);
212+
const callback = (rdbService.buildFromRequestUUIDAndAwait as jasmine.Spy).calls.argsFor(0)[1];
213+
callback();
214+
expect(service.invalidateByHref).toHaveBeenCalledWith('some-href');
215+
216+
done();
225217
});
226218
});
227219

228-
it('should wait for the DELETE request to resolve before emitting', () => {
229-
testScheduler.run(({ cold, expectObservable }) => {
230-
buildFromRequestUUIDSpy.and.returnValue(
231-
cold('----(r|)', { r: MOCK_SUCCEEDED_RD}) // the request takes a while
232-
);
233-
invalidateByHrefSpy.and.returnValue(
234-
cold('(t|)', BOOLEAN) // but we pretend that setting to stale happens sooner
235-
); // e.g.: maybe already stale before this call?
220+
it('should return the RemoteData of the response', (done) => {
221+
buildFromRequestUUIDAndAwaitSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
236222

237-
const done$ = service.deleteByHref('some-href');
238-
expectObservable(done$).toBe(
239-
'----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait for the request
240-
);
223+
service.deleteByHref('some-href').subscribe(rd => {
224+
expect(rd).toBe(MOCK_SUCCEEDED_RD);
225+
done();
241226
});
242227
});
243228
});

src/app/core/data/base/delete-data.ts

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@
66
* http://www.dspace.org/license/
77
*/
88
import { CacheableObject } from '../../cache/cacheable-object.model';
9-
import { AsyncSubject, combineLatest, Observable } from 'rxjs';
9+
import { Observable } from 'rxjs';
1010
import { RemoteData } from '../remote-data';
1111
import { NoContent } from '../../shared/NoContent.model';
12-
import { filter, map, switchMap } from 'rxjs/operators';
12+
import { switchMap } from 'rxjs/operators';
1313
import { DeleteRequest } from '../request.models';
1414
import { hasNoValue, hasValue } from '../../../shared/empty.util';
15-
import { getFirstCompletedRemoteData } from '../../shared/operators';
1615
import { RequestService } from '../request.service';
1716
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
1817
import { HALEndpointService } from '../../shared/hal-endpoint.service';
@@ -83,26 +82,6 @@ export class DeleteDataImpl<T extends CacheableObject> extends IdentifiableDataS
8382
}
8483
this.requestService.send(request);
8584

86-
const response$ = this.rdbService.buildFromRequestUUID(requestId);
87-
88-
const invalidated$ = new AsyncSubject<boolean>();
89-
response$.pipe(
90-
getFirstCompletedRemoteData(),
91-
switchMap((rd: RemoteData<NoContent>) => {
92-
if (rd.hasSucceeded) {
93-
return this.invalidateByHref(href);
94-
} else {
95-
return [true];
96-
}
97-
})
98-
).subscribe(() => {
99-
invalidated$.next(true);
100-
invalidated$.complete();
101-
});
102-
103-
return combineLatest([response$, invalidated$]).pipe(
104-
filter(([_, invalidated]) => invalidated),
105-
map(([response, _]) => response),
106-
);
85+
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => this.invalidateByHref(href));
10786
}
10887
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ describe('BitstreamFormatDataService', () => {
5454
function initTestService(halService) {
5555
rd = createSuccessfulRemoteDataObject({});
5656
rdbService = jasmine.createSpyObj('rdbService', {
57-
buildFromRequestUUID: observableOf(rd)
57+
buildFromRequestUUID: observableOf(rd),
58+
buildFromRequestUUIDAndAwait: observableOf(rd),
5859
});
5960

6061
return new BitstreamFormatDataService(

0 commit comments

Comments
 (0)