Skip to content

Commit f2c2fda

Browse files
authored
Merge pull request #3415 from alexandrevryghem/w2p-107155_Performance-re-request-embeds-main
Added support for caching embedded objects without a self link & null responses
2 parents e589e95 + 1975c63 commit f2c2fda

18 files changed

+109
-76
lines changed

src/app/core/breadcrumbs/item-breadcrumb.resolver.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
import { Observable } from 'rxjs';
88

99
import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
10-
import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page/item.resolver';
10+
import { getItemPageLinksToFollow } from '../../item-page/item.resolver';
1111
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
1212
import { ItemDataService } from '../data/item-data.service';
1313
import { DSpaceObject } from '../shared/dspace-object.model';
@@ -24,7 +24,7 @@ export const itemBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Item>> = (
2424
breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService),
2525
dataService: ItemDataService = inject(ItemDataService),
2626
): Observable<BreadcrumbConfig<Item>> => {
27-
const linksToFollow: FollowLinkConfig<DSpaceObject>[] = ITEM_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig<DSpaceObject>[];
27+
const linksToFollow: FollowLinkConfig<DSpaceObject>[] = getItemPageLinksToFollow() as FollowLinkConfig<DSpaceObject>[];
2828
return DSOBreadcrumbResolver(
2929
route,
3030
state,

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

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { of as observableOf } from 'rxjs';
77
import { TestScheduler } from 'rxjs/testing';
88

99
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
10-
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
1110
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
1211
import {
1312
createSuccessfulRemoteDataObject,
@@ -18,7 +17,6 @@ import {
1817
createPaginatedList,
1918
getFirstUsedArgumentOfSpyMethod,
2019
} from '../../shared/testing/utils.test';
21-
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
2220
import { RequestService } from '../data/request.service';
2321
import { RequestEntry } from '../data/request-entry.model';
2422
import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model';
@@ -31,7 +29,6 @@ describe('BrowseService', () => {
3129
let scheduler: TestScheduler;
3230
let service: BrowseService;
3331
let requestService: RequestService;
34-
let rdbService: RemoteDataBuildService;
3532

3633
const browsesEndpointURL = 'https://rest.api/browses';
3734
const halService: any = new HALEndpointServiceStub(browsesEndpointURL);
@@ -129,7 +126,6 @@ describe('BrowseService', () => {
129126
halService,
130127
browseDefinitionDataService,
131128
hrefOnlyDataService,
132-
rdbService,
133129
);
134130
}
135131

@@ -141,11 +137,9 @@ describe('BrowseService', () => {
141137

142138
beforeEach(() => {
143139
requestService = getMockRequestService(getRequestEntry$(true));
144-
rdbService = getMockRemoteDataBuildService();
145140
service = initTestService();
146141
spyOn(halService, 'getEndpoint').and
147142
.returnValue(hot('--a-', { a: browsesEndpointURL }));
148-
spyOn(rdbService, 'buildList').and.callThrough();
149143
});
150144

151145
it('should call BrowseDefinitionDataService to create the RemoteData Observable', () => {
@@ -162,9 +156,7 @@ describe('BrowseService', () => {
162156

163157
beforeEach(() => {
164158
requestService = getMockRequestService(getRequestEntry$(true));
165-
rdbService = getMockRemoteDataBuildService();
166159
service = initTestService();
167-
spyOn(rdbService, 'buildList').and.callThrough();
168160
});
169161

170162
describe('when getBrowseEntriesFor is called with a valid browse definition id', () => {
@@ -215,7 +207,6 @@ describe('BrowseService', () => {
215207
describe('if getBrowseDefinitions fires', () => {
216208
beforeEach(() => {
217209
requestService = getMockRequestService(getRequestEntry$(true));
218-
rdbService = getMockRemoteDataBuildService();
219210
service = initTestService();
220211
spyOn(service, 'getBrowseDefinitions').and
221212
.returnValue(hot('--a-', {
@@ -270,7 +261,6 @@ describe('BrowseService', () => {
270261
describe('if getBrowseDefinitions doesn\'t fire', () => {
271262
it('should return undefined', () => {
272263
requestService = getMockRequestService(getRequestEntry$(true));
273-
rdbService = getMockRemoteDataBuildService();
274264
service = initTestService();
275265
spyOn(service, 'getBrowseDefinitions').and
276266
.returnValue(hot('----'));
@@ -288,9 +278,7 @@ describe('BrowseService', () => {
288278
describe('getFirstItemFor', () => {
289279
beforeEach(() => {
290280
requestService = getMockRequestService();
291-
rdbService = getMockRemoteDataBuildService();
292281
service = initTestService();
293-
spyOn(rdbService, 'buildList').and.callThrough();
294282
});
295283

296284
describe('when getFirstItemFor is called with a valid browse definition id', () => {

src/app/core/browse/browse.service.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
startWith,
77
} from 'rxjs/operators';
88

9+
import { environment } from '../../../environments/environment';
910
import {
1011
hasValue,
1112
hasValueOperator,
@@ -16,7 +17,6 @@ import {
1617
followLink,
1718
FollowLinkConfig,
1819
} from '../../shared/utils/follow-link-config.model';
19-
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
2020
import { SortDirection } from '../cache/models/sort-options.model';
2121
import { HrefOnlyDataService } from '../data/href-only-data.service';
2222
import { PaginatedList } from '../data/paginated-list.model';
@@ -38,9 +38,15 @@ import { URLCombiner } from '../url-combiner/url-combiner';
3838
import { BrowseDefinitionDataService } from './browse-definition-data.service';
3939
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
4040

41-
export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig<BrowseEntry | Item>[] = [
42-
followLink('thumbnail'),
43-
];
41+
export function getBrowseLinksToFollow(): FollowLinkConfig<BrowseEntry | Item>[] {
42+
const followLinks = [
43+
followLink('thumbnail'),
44+
];
45+
if (environment.item.showAccessStatuses) {
46+
followLinks.push(followLink('accessStatus'));
47+
}
48+
return followLinks;
49+
}
4450

4551
/**
4652
* The service handling all browse requests
@@ -67,7 +73,6 @@ export class BrowseService {
6773
protected halService: HALEndpointService,
6874
private browseDefinitionDataService: BrowseDefinitionDataService,
6975
private hrefOnlyDataService: HrefOnlyDataService,
70-
private rdb: RemoteDataBuildService,
7176
) {
7277
}
7378

@@ -117,7 +122,7 @@ export class BrowseService {
117122
}),
118123
);
119124
if (options.fetchThumbnail ) {
120-
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(href$, {}, undefined, undefined, ...BROWSE_LINKS_TO_FOLLOW);
125+
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(href$, {}, undefined, undefined, ...getBrowseLinksToFollow());
121126
}
122127
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(href$);
123128
}
@@ -165,7 +170,7 @@ export class BrowseService {
165170
}),
166171
);
167172
if (options.fetchThumbnail) {
168-
return this.hrefOnlyDataService.findListByHref<Item>(href$, {}, undefined, undefined, ...BROWSE_LINKS_TO_FOLLOW);
173+
return this.hrefOnlyDataService.findListByHref<Item>(href$, {}, undefined, undefined, ...getBrowseLinksToFollow());
169174
}
170175
return this.hrefOnlyDataService.findListByHref<Item>(href$);
171176
}

src/app/core/cache/object-cache.reducer.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -174,20 +174,25 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi
174174
* the new state, with the object added, or overwritten.
175175
*/
176176
function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState {
177-
const existing = state[action.payload.objectToCache._links.self.href] || {} as any;
177+
const cacheLink = hasValue(action.payload.objectToCache?._links?.self) ? action.payload.objectToCache._links.self.href : action.payload.alternativeLink;
178+
const existing = state[cacheLink] || {} as any;
178179
const newAltLinks = hasValue(action.payload.alternativeLink) ? [action.payload.alternativeLink] : [];
179-
return Object.assign({}, state, {
180-
[action.payload.objectToCache._links.self.href]: {
181-
data: action.payload.objectToCache,
182-
timeCompleted: action.payload.timeCompleted,
183-
msToLive: action.payload.msToLive,
184-
requestUUIDs: [action.payload.requestUUID, ...(existing.requestUUIDs || [])],
185-
dependentRequestUUIDs: existing.dependentRequestUUIDs || [],
186-
isDirty: isNotEmpty(existing.patches),
187-
patches: existing.patches || [],
188-
alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks],
189-
} as ObjectCacheEntry,
190-
});
180+
if (hasValue(cacheLink)) {
181+
return Object.assign({}, state, {
182+
[cacheLink]: {
183+
data: action.payload.objectToCache,
184+
timeCompleted: action.payload.timeCompleted,
185+
msToLive: action.payload.msToLive,
186+
requestUUIDs: [action.payload.requestUUID, ...(existing.requestUUIDs || [])],
187+
dependentRequestUUIDs: existing.dependentRequestUUIDs || [],
188+
isDirty: isNotEmpty(existing.patches),
189+
patches: existing.patches || [],
190+
alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks],
191+
} as ObjectCacheEntry,
192+
});
193+
} else {
194+
return state;
195+
}
191196
}
192197

193198
/**

src/app/core/cache/object-cache.service.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ export class ObjectCacheService {
9999
* An optional alternative link to this object
100100
*/
101101
add(object: CacheableObject, msToLive: number, requestUUID: string, alternativeLink?: string): void {
102-
object = this.linkService.removeResolvedLinks(object); // Ensure the object we're storing has no resolved links
102+
if (hasValue(object)) {
103+
object = this.linkService.removeResolvedLinks(object); // Ensure the object we're storing has no resolved links
104+
}
103105
this.store.dispatch(new AddToObjectCacheAction(object, new Date().getTime(), msToLive, requestUUID, alternativeLink));
104106
}
105107

@@ -175,11 +177,15 @@ export class ObjectCacheService {
175177
},
176178
),
177179
map((entry: ObjectCacheEntry) => {
178-
const type: GenericConstructor<T> = getClassForType((entry.data as any).type);
179-
if (typeof type !== 'function') {
180-
throw new Error(`${type} is not a valid constructor for ${JSON.stringify(entry.data)}`);
180+
if (hasValue(entry.data)) {
181+
const type: GenericConstructor<T> = getClassForType((entry.data as any).type);
182+
if (typeof type !== 'function') {
183+
throw new Error(`${type} is not a valid constructor for ${JSON.stringify(entry.data)}`);
184+
}
185+
return Object.assign(new type(), entry.data) as T;
186+
} else {
187+
return null;
181188
}
182-
return Object.assign(new type(), entry.data) as T;
183189
}),
184190
);
185191
}

src/app/core/data/dspace-rest-response-parsing.service.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ export class DspaceRestResponseParsingService implements ResponseParsingService
120120
if (hasValue(match)) {
121121
embedAltUrl = new URLCombiner(embedAltUrl, `?size=${match.size}`).toString();
122122
}
123+
if (data._embedded[property] == null) {
124+
// Embedded object is null, meaning it exists (not undefined), but had an empty response (204) -> cache it as null
125+
this.addToObjectCache(null, request, data, embedAltUrl);
126+
} else if (!isCacheableObject(data._embedded[property])) {
127+
// Embedded object exists, but doesn't contain a self link -> cache it using the alternative link instead
128+
this.objectCache.add(data._embedded[property], hasValue(request.responseMsToLive) ? request.responseMsToLive : environment.cache.msToLive.default, request.uuid, embedAltUrl);
129+
}
123130
this.process<ObjectDomain>(data._embedded[property], request, embedAltUrl);
124131
});
125132
}
@@ -237,7 +244,7 @@ export class DspaceRestResponseParsingService implements ResponseParsingService
237244
* @param alternativeURL an alternative url that can be used to retrieve the object
238245
*/
239246
addToObjectCache(co: CacheableObject, request: RestRequest, data: any, alternativeURL?: string): void {
240-
if (!isCacheableObject(co)) {
247+
if (hasValue(co) && !isCacheableObject(co)) {
241248
const type = hasValue(data) && hasValue(data.type) ? data.type : 'object';
242249
let dataJSON: string;
243250
if (hasValue(data._embedded)) {
@@ -251,7 +258,7 @@ export class DspaceRestResponseParsingService implements ResponseParsingService
251258
return;
252259
}
253260

254-
if (alternativeURL === co._links.self.href) {
261+
if (hasValue(co) && alternativeURL === co._links.self.href) {
255262
alternativeURL = undefined;
256263
}
257264

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { Store } from '@ngrx/store';
33
import { provideMockStore } from '@ngrx/store/testing';
44
import { of as observableOf } from 'rxjs';
55

6+
import { APP_CONFIG } from '../../../config/app-config.interface';
7+
import { environment } from '../../../environments/environment.test';
68
import { PAGINATED_RELATIONS_TO_ITEMS_OPERATOR } from '../../item-page/simple/item-types/shared/item-relationships-utils';
79
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
810
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
@@ -150,14 +152,15 @@ describe('RelationshipDataService', () => {
150152
{ provide: RequestService, useValue: requestService },
151153
{ provide: PAGINATED_RELATIONS_TO_ITEMS_OPERATOR, useValue: jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v) },
152154
{ provide: Store, useValue: provideMockStore() },
155+
{ provide: APP_CONFIG, useValue: environment },
153156
RelationshipDataService,
154157
],
155158
});
156159
service = TestBed.inject(RelationshipDataService);
157160
});
158161

159162
describe('composition', () => {
160-
const initService = () => new RelationshipDataService(null, null, null, null, null, null, null, null);
163+
const initService = () => new RelationshipDataService(null, null, null, null, null, null, null, null, environment);
161164

162165
testSearchDataImplementation(initService);
163166
});

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ import {
2424
tap,
2525
} from 'rxjs/operators';
2626

27+
import {
28+
APP_CONFIG,
29+
AppConfig,
30+
} from '../../../config/app-config.interface';
2731
import {
2832
AppState,
2933
keySelector,
@@ -133,6 +137,7 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
133137
protected itemService: ItemDataService,
134138
protected appStore: Store<AppState>,
135139
@Inject(PAGINATED_RELATIONS_TO_ITEMS_OPERATOR) private paginatedRelationsToItems: (thisId: string) => (source: Observable<RemoteData<PaginatedList<Relationship>>>) => Observable<RemoteData<PaginatedList<Item>>>,
140+
@Inject(APP_CONFIG) private appConfig: AppConfig,
136141
) {
137142
super('relationships', requestService, rdbService, objectCache, halService, 15 * 60 * 1000);
138143

@@ -319,7 +324,7 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
319324
* @param options
320325
*/
321326
getRelatedItemsByLabel(item: Item, label: string, options?: FindListOptions): Observable<RemoteData<PaginatedList<Item>>> {
322-
const linksToFollow: FollowLinkConfig<Relationship>[] = itemLinksToFollow(options.fetchThumbnail);
327+
const linksToFollow: FollowLinkConfig<Relationship>[] = itemLinksToFollow(options.fetchThumbnail, this.appConfig.item.showAccessStatuses);
323328
linksToFollow.push(followLink('relationshipType'));
324329

325330
return this.getItemRelationshipsByLabel(item, label, options, true, true, ...linksToFollow).pipe(this.paginatedRelationsToItems(item.uuid));

src/app/core/data/version-history-data.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export class VersionHistoryDataService extends IdentifiableDataService<VersionHi
190190
return this.versionDataService.findByHref(versionHref, false, true, followLink('versionhistory')).pipe(
191191
getFirstCompletedRemoteData(),
192192
switchMap((versionRD: RemoteData<Version>) => {
193-
if (versionRD.hasSucceeded && !versionRD.hasNoContent) {
193+
if (versionRD.hasSucceeded && !versionRD.hasNoContent && hasValue(versionRD.payload)) {
194194
return versionRD.payload.versionhistory.pipe(
195195
getFirstCompletedRemoteData(),
196196
map((versionHistoryRD: RemoteData<VersionHistory>) => {

src/app/core/index/index.effects.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class UUIDIndexEffects {
4545
addObject$ = createEffect(() => this.actions$
4646
.pipe(
4747
ofType(ObjectCacheActionTypes.ADD),
48-
filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.uuid)),
48+
filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache) && hasValue(action.payload.objectToCache.uuid)),
4949
map((action: AddToObjectCacheAction) => {
5050
return new AddToIndexAction(
5151
IndexName.OBJECT,
@@ -64,7 +64,7 @@ export class UUIDIndexEffects {
6464
ofType(ObjectCacheActionTypes.ADD),
6565
map((action: AddToObjectCacheAction) => {
6666
const alternativeLink = action.payload.alternativeLink;
67-
const selfLink = action.payload.objectToCache._links.self.href;
67+
const selfLink = hasValue(action.payload.objectToCache?._links?.self) ? action.payload.objectToCache._links.self.href : alternativeLink;
6868
if (hasValue(alternativeLink) && alternativeLink !== selfLink) {
6969
return new AddToIndexAction(
7070
IndexName.ALTERNATIVE_OBJECT_LINK,

0 commit comments

Comments
 (0)