108915: Always invalidate all followLinks when invalidating linked cached object

This commit is contained in:
Alexandre Vryghem
2023-12-08 17:05:00 +01:00
parent e339b46228
commit 24eb5b4bc0
9 changed files with 97 additions and 53 deletions

View File

@@ -21,6 +21,8 @@ import { RequestEntryState } from '../request-entry-state.model';
import { fakeAsync, tick } from '@angular/core/testing';
import { BaseDataService } from './base-data.service';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { ObjectCacheServiceStub } from '../../../shared/testing/object-cache-service.stub';
import { ObjectCacheEntry } from '../../cache/object-cache.reducer';
const endpoint = 'https://rest.api/core';
@@ -46,7 +48,7 @@ describe('BaseDataService', () => {
let requestService;
let halService;
let rdbService;
let objectCache;
let objectCache: ObjectCacheServiceStub;
let selfLink;
let linksToFollow;
let testScheduler;
@@ -56,24 +58,7 @@ describe('BaseDataService', () => {
requestService = getMockRequestService();
halService = new HALEndpointServiceStub('url') as any;
rdbService = getMockRemoteDataBuildService();
objectCache = {
addPatch: () => {
/* empty */
},
getObjectBySelfLink: () => {
/* empty */
},
getByHref: () => {
/* empty */
},
addDependency: () => {
/* empty */
},
removeDependents: () => {
/* empty */
},
} as any;
objectCache = new ObjectCacheServiceStub();
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
linksToFollow = [
followLink('a'),
@@ -104,7 +89,7 @@ describe('BaseDataService', () => {
return new TestService(
requestService,
rdbService,
objectCache,
objectCache as ObjectCacheService,
halService,
);
}
@@ -567,7 +552,7 @@ describe('BaseDataService', () => {
getByHrefSpy = spyOn(objectCache, 'getByHref').and.returnValue(observableOf({
requestUUIDs: ['request1', 'request2', 'request3'],
dependentRequestUUIDs: ['request4', 'request5']
}));
} as ObjectCacheEntry));
});

View File

@@ -24,6 +24,7 @@ import { ObjectCacheEntry } from '../../cache/object-cache.reducer';
import { ObjectCacheService } from '../../cache/object-cache.service';
import { HALDataService } from './hal-data-service.interface';
import { getFirstCompletedRemoteData } from '../../shared/operators';
import { HALLink } from '../../shared/hal-link.model';
export const EMBED_SEPARATOR = '%2F';
/**
@@ -268,7 +269,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
return this.rdbService.buildSingle<T>(requestHref$, ...linksToFollow).pipe(
const response$: Observable<RemoteData<T>> = this.rdbService.buildSingle<T>(requestHref$, ...linksToFollow).pipe(
// This skip ensures that if a stale object is present in the cache when you do a
// call it isn't immediately returned, but we wait until the remote data for the new request
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
@@ -277,6 +278,22 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
this.reRequestStaleRemoteData(reRequestOnStale, () =>
this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
);
return response$.pipe(
// Ensure all followLinks from the cached object are automatically invalidated when invalidating the cached object
tap((remoteDataObject: RemoteData<T>) => {
if (hasValue(remoteDataObject?.payload?._links)) {
for (const followLink of Object.values(remoteDataObject.payload._links)) {
// followLink can be either an individual HALLink or a HALLink[]
const followLinksList: HALLink[] = [].concat(followLink);
for (const individualFollowLink of followLinksList) {
if (hasValue(individualFollowLink?.href)) {
this.addDependency(response$, individualFollowLink.href);
}
}
}
}
}),
);
}
/**
@@ -302,7 +319,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
return this.rdbService.buildList<T>(requestHref$, ...linksToFollow).pipe(
const response$: Observable<RemoteData<PaginatedList<T>>> = this.rdbService.buildList<T>(requestHref$, ...linksToFollow).pipe(
// This skip ensures that if a stale object is present in the cache when you do a
// call it isn't immediately returned, but we wait until the remote data for the new request
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
@@ -311,6 +328,26 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
this.reRequestStaleRemoteData(reRequestOnStale, () =>
this.findListByHref(href$, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
);
return response$.pipe(
// Ensure all followLinks from the cached object are automatically invalidated when invalidating the cached object
tap((remoteDataObject: RemoteData<PaginatedList<T>>) => {
if (hasValue(remoteDataObject?.payload?.page)) {
for (const object of remoteDataObject.payload.page) {
if (hasValue(object?._links)) {
for (const followLink of Object.values(object._links)) {
// followLink can be either an individual HALLink or a HALLink[]
const followLinksList: HALLink[] = [].concat(followLink);
for (const individualFollowLink of followLinksList) {
if (hasValue(individualFollowLink?.href)) {
this.addDependency(response$, individualFollowLink.href);
}
}
}
}
}
}
}),
);
}
/**

View File

@@ -24,6 +24,7 @@ import { testFindAllDataImplementation } from './base/find-all-data.spec';
import { testSearchDataImplementation } from './base/search-data.spec';
import { testPatchDataImplementation } from './base/patch-data.spec';
import { testDeleteDataImplementation } from './base/delete-data.spec';
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
const url = 'fake-url';
const collectionId = 'fake-collection-id';
@@ -35,7 +36,7 @@ describe('CollectionDataService', () => {
let translate: TranslateService;
let notificationsService: any;
let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService;
let objectCache: ObjectCacheServiceStub;
let halService: any;
const mockCollection1: Collection = Object.assign(new Collection(), {
@@ -205,14 +206,12 @@ describe('CollectionDataService', () => {
buildFromRequestUUID: buildResponse$,
buildSingle: buildResponse$
});
objectCache = jasmine.createSpyObj('objectCache', {
remove: jasmine.createSpy('remove')
});
objectCache = new ObjectCacheServiceStub();
halService = new HALEndpointServiceStub(url);
notificationsService = new NotificationsServiceStub();
translate = getMockTranslateService();
service = new CollectionDataService(requestService, rdbService, objectCache, halService, null, notificationsService, null, null, translate);
service = new CollectionDataService(requestService, rdbService, objectCache as ObjectCacheService, halService, null, notificationsService, null, null, translate);
}
});

View File

@@ -23,6 +23,7 @@ import { FindListOptions } from './find-list-options.model';
import { testSearchDataImplementation } from './base/search-data.spec';
import { MetadataValue } from '../shared/metadata.models';
import { MetadataRepresentationType } from '../shared/metadata-representation/metadata-representation.model';
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
describe('RelationshipDataService', () => {
let service: RelationshipDataService;
@@ -114,14 +115,7 @@ describe('RelationshipDataService', () => {
'href': buildList$,
'https://rest.api/core/publication/relationships': relationships$
});
const objectCache = Object.assign({
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
remove: () => {
},
hasBySelfLinkObservable: () => observableOf(false),
hasByHref$: () => observableOf(false)
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
}) as ObjectCacheService;
const objectCache = new ObjectCacheServiceStub();
const itemService = jasmine.createSpyObj('itemService', {
findById: (uuid) => createSuccessfulRemoteDataObject(relatedItems.find((relatedItem) => relatedItem.id === uuid)),
@@ -133,7 +127,7 @@ describe('RelationshipDataService', () => {
requestService,
rdbService,
halService,
objectCache,
objectCache as ObjectCacheService,
itemService,
null,
jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v),

View File

@@ -10,6 +10,7 @@ import { RequestService } from './request.service';
import { createPaginatedList } from '../../shared/testing/utils.test';
import { hasValueOperator } from '../../shared/empty.util';
import { ObjectCacheService } from '../cache/object-cache.service';
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
describe('RelationshipTypeDataService', () => {
let service: RelationshipTypeDataService;
@@ -28,7 +29,7 @@ describe('RelationshipTypeDataService', () => {
let buildList;
let rdbService;
let objectCache;
let objectCache: ObjectCacheServiceStub;
function init() {
restEndpointURL = 'https://rest.api/relationshiptypes';
@@ -60,21 +61,14 @@ describe('RelationshipTypeDataService', () => {
buildList = createSuccessfulRemoteDataObject(createPaginatedList([relationshipType1, relationshipType2]));
rdbService = getMockRemoteDataBuildService(undefined, observableOf(buildList));
objectCache = Object.assign({
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
remove: () => {
},
hasBySelfLinkObservable: () => observableOf(false)
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
}) as ObjectCacheService;
objectCache = new ObjectCacheServiceStub();
}
function initTestService() {
return new RelationshipTypeDataService(
requestService,
rdbService,
objectCache,
objectCache as ObjectCacheService,
halService,
);
}

View File

@@ -20,13 +20,14 @@ import { FindListOptions } from '../data/find-list-options.model';
import { EPersonDataService } from '../eperson/eperson-data.service';
import { GroupDataService } from '../eperson/group-data.service';
import { RestRequestMethod } from '../data/rest-request-method';
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
describe('ResourcePolicyService', () => {
let scheduler: TestScheduler;
let service: ResourcePolicyDataService;
let requestService: RequestService;
let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService;
let objectCache: ObjectCacheServiceStub;
let halService: HALEndpointService;
let responseCacheEntry: RequestEntry;
let ePersonService: EPersonDataService;
@@ -139,14 +140,14 @@ describe('ResourcePolicyService', () => {
a: 'https://rest.api/rest/api/eperson/groups/' + groupUUID
}),
});
objectCache = {} as ObjectCacheService;
objectCache = new ObjectCacheServiceStub();
const notificationsService = {} as NotificationsService;
const comparator = {} as any;
service = new ResourcePolicyDataService(
requestService,
rdbService,
objectCache,
objectCache as ObjectCacheService,
halService,
notificationsService,
comparator,

View File

@@ -25,6 +25,7 @@ import { createPaginatedList } from '../../../shared/testing/utils.test';
import { RequestEntry } from '../../data/request-entry.model';
import { VocabularyDataService } from './vocabulary.data.service';
import { VocabularyEntryDetailsDataService } from './vocabulary-entry-details.data.service';
import { ObjectCacheServiceStub } from '../../../shared/testing/object-cache-service.stub';
describe('VocabularyService', () => {
let scheduler: TestScheduler;
@@ -205,6 +206,7 @@ describe('VocabularyService', () => {
function initTestService() {
hrefOnlyDataService = getMockHrefOnlyDataService();
objectCache = new ObjectCacheServiceStub() as ObjectCacheService;
return new VocabularyService(
requestService,

View File

@@ -17,13 +17,14 @@ import { RestResponse } from '../cache/response.models';
import { RequestEntry } from '../data/request-entry.model';
import { FindListOptions } from '../data/find-list-options.model';
import { GroupDataService } from '../eperson/group-data.service';
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
describe('SupervisionOrderService', () => {
let scheduler: TestScheduler;
let service: SupervisionOrderDataService;
let requestService: RequestService;
let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService;
let objectCache: ObjectCacheServiceStub;
let halService: HALEndpointService;
let responseCacheEntry: RequestEntry;
let groupService: GroupDataService;
@@ -127,14 +128,14 @@ describe('SupervisionOrderService', () => {
a: 'https://rest.api/rest/api/group/groups/' + groupUUID
}),
});
objectCache = {} as ObjectCacheService;
objectCache = new ObjectCacheServiceStub();
const notificationsService = {} as NotificationsService;
const comparator = {} as any;
service = new SupervisionOrderDataService(
requestService,
rdbService,
objectCache,
objectCache as ObjectCacheService,
halService,
notificationsService,
comparator,

View File

@@ -0,0 +1,31 @@
import { Observable, of as observableOf } from 'rxjs';
import { CacheableObject } from '../../core/cache/cacheable-object.model';
import { ObjectCacheEntry } from '../../core/cache/object-cache.reducer';
/* eslint-disable @typescript-eslint/no-empty-function */
/**
* Stub class of {@link ObjectCacheService}
*/
export class ObjectCacheServiceStub {
add(_object: CacheableObject, _msToLive: number, _requestUUID: string, _alternativeLink?: string): void {
}
remove(_href: string): void {
}
getByHref(_href: string): Observable<ObjectCacheEntry> {
return observableOf(undefined);
}
hasByHref$(_href: string): Observable<boolean> {
return observableOf(false);
}
addDependency(_href$: string | Observable<string>, _dependsOnHref$: string | Observable<string>): void {
}
removeDependents(_href: string): void {
}
}