94273: Implement dependencies between requests

When an object is invalidated, its dependent requests are invalidated as well
This commit is contained in:
Yury Bondarenko
2022-09-08 13:25:41 +02:00
parent 8622e4c059
commit d40f163c49
9 changed files with 468 additions and 48 deletions

View File

@@ -13,7 +13,9 @@ export const ObjectCacheActionTypes = {
REMOVE: type('dspace/core/cache/object/REMOVE'),
RESET_TIMESTAMPS: type('dspace/core/cache/object/RESET_TIMESTAMPS'),
ADD_PATCH: type('dspace/core/cache/object/ADD_PATCH'),
APPLY_PATCH: type('dspace/core/cache/object/APPLY_PATCH')
APPLY_PATCH: type('dspace/core/cache/object/APPLY_PATCH'),
ADD_DEPENDENTS: type('dspace/core/cache/object/ADD_DEPENDENTS'),
REMOVE_DEPENDENTS: type('dspace/core/cache/object/REMOVE_DEPENDENTS')
};
/**
@@ -126,13 +128,49 @@ export class ApplyPatchObjectCacheAction implements Action {
}
}
export class AddDependentsObjectCacheAction implements Action {
type = ObjectCacheActionTypes.ADD_DEPENDENTS;
payload: {
href: string;
dependentRequestUUIDs: string[];
};
/**
* Create a new AddDependencyObjectCacheAction
*
* @param href the self link of a cached object
* @param dependentRequestUUIDs the UUID of the request that depends on this object
*/
constructor(href: string, dependentRequestUUIDs: string[]) {
this.payload = {
href,
dependentRequestUUIDs,
};
}
}
export class RemoveDependentsObjectCacheAction implements Action {
type = ObjectCacheActionTypes.REMOVE_DEPENDENTS;
payload: string;
/**
* Create a new AddDependencyObjectCacheAction
*
* @param href the self link of a cached object for which to remove all dependent request UUIDs
*/
constructor(href: string) {
this.payload = href;
}
}
/**
* A type to encompass all ObjectCacheActions
*/
export type ObjectCacheAction
= AddToObjectCacheAction
| RemoveFromObjectCacheAction
| ResetObjectCacheTimestampsAction
| AddPatchObjectCacheAction
| ApplyPatchObjectCacheAction;
| RemoveFromObjectCacheAction
| ResetObjectCacheTimestampsAction
| AddPatchObjectCacheAction
| ApplyPatchObjectCacheAction
| AddDependentsObjectCacheAction
| RemoveDependentsObjectCacheAction;

View File

@@ -2,11 +2,13 @@ import * as deepFreeze from 'deep-freeze';
import { Operation } from 'fast-json-patch';
import { Item } from '../shared/item.model';
import {
AddDependentsObjectCacheAction,
AddPatchObjectCacheAction,
AddToObjectCacheAction,
ApplyPatchObjectCacheAction,
RemoveDependentsObjectCacheAction,
RemoveFromObjectCacheAction,
ResetObjectCacheTimestampsAction
ResetObjectCacheTimestampsAction,
} from './object-cache.actions';
import { objectCacheReducer } from './object-cache.reducer';
@@ -42,20 +44,22 @@ describe('objectCacheReducer', () => {
timeCompleted: new Date().getTime(),
msToLive: 900000,
requestUUIDs: [requestUUID1],
dependentRequestUUIDs: [],
patches: [],
isDirty: false,
},
[selfLink2]: {
data: {
type: Item.type,
self: requestUUID2,
self: selfLink2,
foo: 'baz',
_links: { self: { href: requestUUID2 } }
_links: { self: { href: selfLink2 } }
},
alternativeLinks: [altLink3, altLink4],
timeCompleted: new Date().getTime(),
msToLive: 900000,
requestUUIDs: [selfLink2],
requestUUIDs: [requestUUID2],
dependentRequestUUIDs: [requestUUID1],
patches: [],
isDirty: false
}
@@ -189,4 +193,20 @@ describe('objectCacheReducer', () => {
expect((newState[selfLink1].data as any).name).toEqual(newName);
});
it('should add dependent requests on ADD_DEPENDENTS', () => {
let newState = objectCacheReducer(testState, new AddDependentsObjectCacheAction(selfLink1, ['new', 'newer', 'newest']));
expect(newState[selfLink1].dependentRequestUUIDs).toEqual(['new', 'newer', 'newest']);
newState = objectCacheReducer(newState, new AddDependentsObjectCacheAction(selfLink2, ['more']));
expect(newState[selfLink2].dependentRequestUUIDs).toEqual([requestUUID1, 'more']);
});
it('should clear dependent requests on REMOVE_DEPENDENTS', () => {
let newState = objectCacheReducer(testState, new RemoveDependentsObjectCacheAction(selfLink1));
expect(newState[selfLink1].dependentRequestUUIDs).toEqual([]);
newState = objectCacheReducer(newState, new RemoveDependentsObjectCacheAction(selfLink2));
expect(newState[selfLink2].dependentRequestUUIDs).toEqual([]);
});
});

View File

@@ -1,12 +1,13 @@
/* eslint-disable max-classes-per-file */
import {
AddDependentsObjectCacheAction,
AddPatchObjectCacheAction,
AddToObjectCacheAction,
ApplyPatchObjectCacheAction,
ObjectCacheAction,
ObjectCacheActionTypes,
ObjectCacheActionTypes, RemoveDependentsObjectCacheAction,
RemoveFromObjectCacheAction,
ResetObjectCacheTimestampsAction
ResetObjectCacheTimestampsAction,
} from './object-cache.actions';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { CacheEntry } from './cache-entry';
@@ -69,6 +70,12 @@ export class ObjectCacheEntry implements CacheEntry {
*/
requestUUIDs: string[];
/**
* A list of UUIDs for the requests that depend on this object.
* When this object is invalidated, these requests will be invalidated as well.
*/
dependentRequestUUIDs: string[];
/**
* An array of patches that were made on the client side to this entry, but haven't been sent to the server yet
*/
@@ -134,6 +141,14 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi
return applyPatchObjectCache(state, action as ApplyPatchObjectCacheAction);
}
case ObjectCacheActionTypes.ADD_DEPENDENTS: {
return addDependentsObjectCacheState(state, action as AddDependentsObjectCacheAction);
}
case ObjectCacheActionTypes.REMOVE_DEPENDENTS: {
return removeDependentsObjectCacheState(state, action as RemoveDependentsObjectCacheAction);
}
default: {
return state;
}
@@ -159,6 +174,7 @@ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheActio
timeCompleted: action.payload.timeCompleted,
msToLive: action.payload.msToLive,
requestUUIDs: [action.payload.requestUUID, ...(existing.requestUUIDs || [])],
dependentRequestUUIDs: existing.dependentRequestUUIDs || [],
isDirty: isNotEmpty(existing.patches),
patches: existing.patches || [],
alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks]
@@ -252,3 +268,49 @@ function applyPatchObjectCache(state: ObjectCacheState, action: ApplyPatchObject
}
return newState;
}
/**
* Add a list of dependent request UUIDs to a cached object, used when defining new dependencies
*
* @param state the current state
* @param action an AddDependentsObjectCacheAction
* @return the new state, with the dependent requests of the cached object updated
*/
function addDependentsObjectCacheState(state: ObjectCacheState, action: AddDependentsObjectCacheAction): ObjectCacheState {
const href = action.payload.href;
const newState = Object.assign({}, state);
if (hasValue(newState[href])) {
newState[href] = Object.assign({}, newState[href], {
dependentRequestUUIDs: [
...new Set([
...newState[href]?.dependentRequestUUIDs || [],
...action.payload.dependentRequestUUIDs,
])
]
});
}
return newState;
}
/**
* Remove all dependent request UUIDs from a cached object, used to clear out-of-date depedencies
*
* @param state the current state
* @param action an AddDependentsObjectCacheAction
* @return the new state, with the dependent requests of the cached object updated
*/
function removeDependentsObjectCacheState(state: ObjectCacheState, action: RemoveDependentsObjectCacheAction): ObjectCacheState {
const href = action.payload;
const newState = Object.assign({}, state);
if (hasValue(newState[href])) {
newState[href] = Object.assign({}, newState[href], {
dependentRequestUUIDs: []
});
}
return newState;
}

View File

@@ -11,10 +11,12 @@ import { coreReducers} from '../core.reducers';
import { RestRequestMethod } from '../data/rest-request-method';
import { Item } from '../shared/item.model';
import {
AddDependentsObjectCacheAction,
RemoveDependentsObjectCacheAction,
AddPatchObjectCacheAction,
AddToObjectCacheAction,
ApplyPatchObjectCacheAction,
RemoveFromObjectCacheAction
RemoveFromObjectCacheAction,
} from './object-cache.actions';
import { Patch } from './object-cache.reducer';
import { ObjectCacheService } from './object-cache.service';
@@ -25,6 +27,7 @@ import { storeModuleConfig } from '../../app.reducer';
import { TestColdObservable } from 'jasmine-marbles/src/test-observables';
import { IndexName } from '../index/index-name.model';
import { CoreState } from '../core-state.model';
import { TestScheduler } from 'rxjs/testing';
describe('ObjectCacheService', () => {
let service: ObjectCacheService;
@@ -38,6 +41,7 @@ describe('ObjectCacheService', () => {
let altLink1;
let altLink2;
let requestUUID;
let requestUUID2;
let alternativeLink;
let timestamp;
let timestamp2;
@@ -55,6 +59,7 @@ describe('ObjectCacheService', () => {
altLink1 = 'https://alternative.link/endpoint/1234';
altLink2 = 'https://alternative.link/endpoint/5678';
requestUUID = '4d3a4ce8-a375-4b98-859b-39f0a014d736';
requestUUID2 = 'c0f486c1-c4d3-4a03-b293-ca5b71ff0054';
alternativeLink = 'https://rest.api/endpoint/5e4f8a5-be98-4c51-9fd8-6bfedcbd59b7/item';
timestamp = new Date().getTime();
timestamp2 = new Date().getTime() - 200;
@@ -71,13 +76,17 @@ describe('ObjectCacheService', () => {
data: objectToCache,
timeCompleted: timestamp,
msToLive: msToLive,
alternativeLinks: [altLink1, altLink2]
alternativeLinks: [altLink1, altLink2],
requestUUIDs: [requestUUID],
dependentRequestUUIDs: [],
};
cacheEntry2 = {
data: objectToCache,
timeCompleted: timestamp2,
msToLive: msToLive2,
alternativeLinks: [altLink2]
alternativeLinks: [altLink2],
requestUUIDs: [requestUUID2],
dependentRequestUUIDs: [],
};
invalidCacheEntry = Object.assign({}, cacheEntry, { msToLive: -1 });
operations = [{ op: 'replace', path: '/name', value: 'random string' } as Operation];
@@ -343,4 +352,122 @@ describe('ObjectCacheService', () => {
expect(store.dispatch).toHaveBeenCalledWith(new ApplyPatchObjectCacheAction(selfLink));
});
});
describe('request dependencies', () => {
beforeEach(() => {
const state = Object.assign({}, initialState, {
core: Object.assign({}, initialState.core, {
'cache/object': {
['objectWithoutDependents']: {
dependentRequestUUIDs: [],
},
['objectWithDependents']: {
dependentRequestUUIDs: [requestUUID],
},
[selfLink]: cacheEntry,
},
'index': {
'object/alt-link-to-self-link': {
[anotherLink]: selfLink,
['objectWithoutDependentsAlt']: 'objectWithoutDependents',
['objectWithDependentsAlt']: 'objectWithDependents',
}
}
})
});
mockStore.setState(state);
});
describe('addDependency', () => {
it('should dispatch an ADD_DEPENDENTS action', () => {
service.addDependency(selfLink, 'objectWithoutDependents');
expect(store.dispatch).toHaveBeenCalledOnceWith(new AddDependentsObjectCacheAction('objectWithoutDependents', [requestUUID]));
});
it('should resolve alt links', () => {
service.addDependency(anotherLink, 'objectWithoutDependentsAlt');
expect(store.dispatch).toHaveBeenCalledOnceWith(new AddDependentsObjectCacheAction('objectWithoutDependents', [requestUUID]));
});
it('should not dispatch if either href cannot be resolved to a cached self link', () => {
service.addDependency(selfLink, 'unknown');
service.addDependency('unknown', 'objectWithoutDependents');
service.addDependency('nothing', 'matches');
expect(store.dispatch).not.toHaveBeenCalled();
});
it('should not dispatch if either href is undefined', () => {
service.addDependency(selfLink, undefined);
service.addDependency(undefined, 'objectWithoutDependents');
service.addDependency(undefined, undefined);
expect(store.dispatch).not.toHaveBeenCalled();
});
it('should not dispatch if the dependency exists already', () => {
service.addDependency(selfLink, 'objectWithDependents');
expect(store.dispatch).not.toHaveBeenCalled();
});
it('should work with observable hrefs', () => {
service.addDependency(observableOf(selfLink), observableOf('objectWithoutDependents'));
expect(store.dispatch).toHaveBeenCalledOnceWith(new AddDependentsObjectCacheAction('objectWithoutDependents', [requestUUID]));
});
it('should only dispatch once for the first value of either observable href', () => {
const testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
testScheduler.run(({ cold: tsCold, flush }) => {
const href$ = tsCold('--y-n-n', {
y: selfLink,
n: 'NOPE'
});
const dependsOnHref$ = tsCold('-y-n-n', {
y: 'objectWithoutDependents',
n: 'NOPE'
});
service.addDependency(href$, dependsOnHref$);
flush();
expect(store.dispatch).toHaveBeenCalledOnceWith(new AddDependentsObjectCacheAction('objectWithoutDependents', [requestUUID]));
});
});
it('should not dispatch if either of the hrefs emits undefined', () => {
const testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
testScheduler.run(({ cold: tsCold, flush }) => {
const undefined$ = tsCold('--u');
service.addDependency(selfLink, undefined$);
service.addDependency(undefined$, 'objectWithoutDependents');
service.addDependency(undefined$, undefined$);
flush();
expect(store.dispatch).not.toHaveBeenCalled();
});
});
});
describe('removeDependents', () => {
it('should dispatch a REMOVE_DEPENDENTS action', () => {
service.removeDependents('objectWithDependents');
expect(store.dispatch).toHaveBeenCalledOnceWith(new RemoveDependentsObjectCacheAction('objectWithDependents'));
});
it('should resolve alt links', () => {
service.removeDependents('objectWithDependentsAlt');
expect(store.dispatch).toHaveBeenCalledOnceWith(new RemoveDependentsObjectCacheAction('objectWithDependents'));
});
it('should not dispatch if the href cannot be resolved to a cached self link', () => {
service.removeDependents('unknown');
expect(store.dispatch).not.toHaveBeenCalled();
});
});
});
});

View File

@@ -4,23 +4,15 @@ import { applyPatch, Operation } from 'fast-json-patch';
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, switchMap, take } from 'rxjs/operators';
import { hasValue, isNotEmpty, isEmpty } from '../../shared/empty.util';
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { CoreState } from '../core-state.model';
import { coreSelector } from '../core.selectors';
import { RestRequestMethod } from '../data/rest-request-method';
import {
selfLinkFromAlternativeLinkSelector,
selfLinkFromUuidSelector
} from '../index/index.selectors';
import { selfLinkFromAlternativeLinkSelector, selfLinkFromUuidSelector } from '../index/index.selectors';
import { GenericConstructor } from '../shared/generic-constructor';
import { getClassForType } from './builders/build-decorators';
import { LinkService } from './builders/link.service';
import {
AddPatchObjectCacheAction,
AddToObjectCacheAction,
ApplyPatchObjectCacheAction,
RemoveFromObjectCacheAction
} from './object-cache.actions';
import { AddDependentsObjectCacheAction, AddPatchObjectCacheAction, AddToObjectCacheAction, ApplyPatchObjectCacheAction, RemoveDependentsObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions';
import { ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer';
import { AddToSSBAction } from './server-sync-buffer.actions';
@@ -339,4 +331,97 @@ export class ObjectCacheService {
this.store.dispatch(new ApplyPatchObjectCacheAction(selfLink));
}
/**
* Add a new dependency between two cached objects.
* When {@link dependsOnHref$} is invalidated, {@link href$} will be invalidated as well.
*
* This method should be called _after_ requests have been sent;
* it will only work if both objects are already present in the cache.
*
* If either object is undefined, the dependency will not be added
*
* @param href$ the href of an object to add a dependency to
* @param dependsOnHref$ the href of the new dependency
*/
addDependency(href$: string | Observable<string>, dependsOnHref$: string | Observable<string>) {
if (hasNoValue(href$) || hasNoValue(dependsOnHref$)) {
return;
}
if (typeof href$ === 'string') {
href$ = observableOf(href$);
}
if (typeof dependsOnHref$ === 'string') {
dependsOnHref$ = observableOf(dependsOnHref$);
}
observableCombineLatest([
href$,
dependsOnHref$.pipe(
switchMap(dependsOnHref => this.resolveSelfLink(dependsOnHref))
),
]).pipe(
switchMap(([href, dependsOnSelfLink]: [string, string]) => {
const dependsOnSelfLink$ = observableOf(dependsOnSelfLink);
return observableCombineLatest([
dependsOnSelfLink$,
dependsOnSelfLink$.pipe(
switchMap(selfLink => this.getBySelfLink(selfLink)),
map(oce => oce?.dependentRequestUUIDs || []),
),
this.getByHref(href).pipe(
// only add the latest request to keep dependency index from growing indefinitely
map((entry: ObjectCacheEntry) => entry?.requestUUIDs?.[0]),
)
]);
}),
take(1),
).subscribe(([dependsOnSelfLink, currentDependents, newDependent]: [string, string[], string]) => {
// don't dispatch if either href is invalid or if the new dependency already exists
if (hasValue(dependsOnSelfLink) && hasValue(newDependent) && !currentDependents.includes(newDependent)) {
this.store.dispatch(new AddDependentsObjectCacheAction(dependsOnSelfLink, [newDependent]));
}
});
}
/**
* Clear all dependent requests associated with a cache entry.
*
* @href the href of a cached object
*/
removeDependents(href: string) {
this.resolveSelfLink(href).pipe(
take(1),
).subscribe((selfLink: string) => {
if (hasValue(selfLink)) {
this.store.dispatch(new RemoveDependentsObjectCacheAction(selfLink));
}
});
}
/**
* Resolve the self link of an existing cached object from an arbitrary href
*
* @param href any href
* @return an observable of the self link corresponding to the given href.
* Will emit the given href if it was a self link, another href
* if the given href was an alt link, or undefined if there is no
* cached object for this href.
*/
private resolveSelfLink(href: string): Observable<string> {
return this.getBySelfLink(href).pipe(
switchMap((oce: ObjectCacheEntry) => {
if (isNotEmpty(oce)) {
return [href];
} else {
return this.store.pipe(
select(selfLinkFromAlternativeLinkSelector(href)),
);
}
}),
);
}
}

View File

@@ -2,7 +2,7 @@
import { HttpClient } from '@angular/common/http';
import { Store } from '@ngrx/store';
import { compare, Operation } from 'fast-json-patch';
import { Observable, of as observableOf } from 'rxjs';
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@@ -13,6 +13,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Item } from '../shared/item.model';
import {
createFailedRemoteDataObject,
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$,
} from '../../shared/remote-data.utils';
@@ -96,7 +97,13 @@ describe('DataService', () => {
},
getByHref: () => {
/* empty */
}
},
addDependency: () => {
/* empty */
},
removeDependents: () => {
/* empty */
},
} as any;
store = {} as Store<CoreState>;
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
@@ -849,7 +856,8 @@ describe('DataService', () => {
beforeEach(() => {
getByHrefSpy = spyOn(objectCache, 'getByHref').and.returnValue(observableOf({
requestUUIDs: ['request1', 'request2', 'request3']
requestUUIDs: ['request1', 'request2', 'request3'],
dependentRequestUUIDs: []
}));
});
@@ -898,9 +906,9 @@ describe('DataService', () => {
it('should only fire for the current state of the object (instead of tracking it)', () => {
testScheduler.run(({ cold, flush }) => {
getByHrefSpy.and.returnValue(cold('a---b---c---', {
a: { requestUUIDs: ['request1'] }, // this is the state at the moment we're invalidating the cache
b: { requestUUIDs: ['request2'] }, // we shouldn't keep tracking the state
c: { requestUUIDs: ['request3'] }, // because we may invalidate when we shouldn't
a: { requestUUIDs: ['request1'], dependentRequestUUIDs: [] }, // this is the state at the moment we're invalidating the cache
b: { requestUUIDs: ['request2'], dependentRequestUUIDs: [] }, // we shouldn't keep tracking the state
c: { requestUUIDs: ['request3'], dependentRequestUUIDs: [] }, // because we may invalidate when we shouldn't
}));
service.invalidateByHref('some-href');
@@ -970,4 +978,42 @@ describe('DataService', () => {
});
});
});
describe('addDependency', () => {
let addDependencySpy;
beforeEach(() => {
addDependencySpy = spyOn(objectCache, 'addDependency');
});
it('should call objectCache.addDependency with the object\'s self link', () => {
addDependencySpy.and.callFake((href$: Observable<string>, dependsOn$: Observable<string>) => {
observableCombineLatest([href$, dependsOn$]).subscribe(([href, dependsOn]) => {
expect(href).toBe('object-href');
expect(dependsOn).toBe('dependsOnHref');
});
});
(service as any).addDependency(
createSuccessfulRemoteDataObject$({ _links: { self: { href: 'object-href' } } }),
observableOf('dependsOnHref')
);
expect(addDependencySpy).toHaveBeenCalled();
});
it('should call objectCache.addDependency without an href if request failed', () => {
addDependencySpy.and.callFake((href$: Observable<string>, dependsOn$: Observable<string>) => {
observableCombineLatest([href$, dependsOn$]).subscribe(([href, dependsOn]) => {
expect(href).toBe(undefined);
expect(dependsOn).toBe('dependsOnHref');
});
});
(service as any).addDependency(
createFailedRemoteDataObject$('something went wrong'),
observableOf('dependsOnHref')
);
expect(addDependencySpy).toHaveBeenCalled();
});
});
});

View File

@@ -27,7 +27,7 @@ import { ObjectCacheService } from '../cache/object-cache.service';
import { DSpaceSerializer } from '../dspace-rest/dspace.serializer';
import { DSpaceObject } from '../shared/dspace-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators';
import { getFirstSucceededRemoteData, getRemoteDataPayload, getFirstCompletedRemoteData } from '../shared/operators';
import { URLCombiner } from '../url-combiner/url-combiner';
import { ChangeAnalyzer } from './change-analyzer';
import { PaginatedList } from './paginated-list.model';
@@ -576,6 +576,35 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
return result$;
}
/**
* Shorthand method to add a dependency to a cached object
* ```
* const out$ = this.findByHref(...); // or another method that sends a request
* this.addDependency(out$, dependsOnHref);
* ```
* When {@link dependsOnHref$} is invalidated, {@link object$} will be invalidated as well.
*
*
* @param object$ the cached object
* @param dependsOnHref$ the href of the object it should depend on
*/
protected addDependency(object$: Observable<RemoteData<T | PaginatedList<T>>>, dependsOnHref$: string | Observable<string>) {
this.objectCache.addDependency(
object$.pipe(
getFirstCompletedRemoteData(),
switchMap((rd: RemoteData<T>) => {
if (rd.hasSucceeded) {
return [rd.payload._links.self.href];
} else {
// undefined href will be skipped in objectCache.addDependency
return [undefined];
}
}),
),
dependsOnHref$
);
}
/**
* Invalidate an existing DSpaceObject by marking all requests it is included in as stale
* @param objectId The id of the object to be invalidated
@@ -597,11 +626,17 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
this.objectCache.getByHref(href).pipe(
take(1),
switchMap((oce: ObjectCacheEntry) => observableFrom(oce.requestUUIDs).pipe(
mergeMap((requestUUID: string) => this.requestService.setStaleByUUID(requestUUID)),
toArray(),
)),
switchMap((oce: ObjectCacheEntry) => {
return observableFrom([
...oce.requestUUIDs,
...oce.dependentRequestUUIDs
]).pipe(
mergeMap((requestUUID: string) => this.requestService.setStaleByUUID(requestUUID)),
toArray(),
);
}),
).subscribe(() => {
this.objectCache.removeDependents(href);
done$.next(true);
done$.complete();
});

View File

@@ -27,11 +27,10 @@ import { createPaginatedList, createRequestEntry$ } from '../../shared/testing/u
import { CoreState } from '../core-state.model';
import { FindListOptions } from '../data/find-list-options.model';
import { DataService } from '../data/data.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { getMockLinkService } from '../../shared/mocks/link-service.mock';
import { DataServiceStub } from '../../shared/testing/data-service.stub';
import { of as observableOf } from 'rxjs';
import { ObjectCacheEntry } from '../cache/object-cache.reducer';
import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock';
describe('GroupDataService', () => {
let service: GroupDataService;
@@ -44,7 +43,7 @@ describe('GroupDataService', () => {
let groups$;
let halService;
let rdbService;
let objectCache: ObjectCacheService;
let objectCache;
let dataService: DataServiceStub;
function init() {
@@ -54,7 +53,9 @@ describe('GroupDataService', () => {
groups$ = createSuccessfulRemoteDataObject$(createPaginatedList(groups));
rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups': groups$ });
halService = new HALEndpointServiceStub(restEndpointURL);
objectCache = new ObjectCacheService(store, getMockLinkService());
objectCache = getMockObjectCacheService();
dataService = new DataServiceStub();
TestBed.configureTestingModule({
imports: [
@@ -122,8 +123,9 @@ describe('GroupDataService', () => {
describe('addSubGroupToGroup', () => {
beforeEach(() => {
spyOn(objectCache, 'getByHref').and.returnValue(observableOf({
requestUUIDs: ['request1', 'request2']
objectCache.getByHref.and.returnValue(observableOf({
requestUUIDs: ['request1', 'request2'],
dependentRequestUUIDs: [],
} as ObjectCacheEntry));
spyOn(dataService, 'invalidateByHref');
service.addSubGroupToGroup(GroupMock, GroupMock2).subscribe();
@@ -151,8 +153,9 @@ describe('GroupDataService', () => {
describe('deleteSubGroupFromGroup', () => {
beforeEach(() => {
spyOn(objectCache, 'getByHref').and.returnValue(observableOf({
requestUUIDs: ['request1', 'request2']
objectCache.getByHref.and.returnValue(observableOf({
requestUUIDs: ['request1', 'request2'],
dependentRequestUUIDs: [],
} as ObjectCacheEntry));
spyOn(dataService, 'invalidateByHref');
service.deleteSubGroupFromGroup(GroupMock, GroupMock2).subscribe();
@@ -176,8 +179,9 @@ describe('GroupDataService', () => {
describe('addMemberToGroup', () => {
beforeEach(() => {
spyOn(objectCache, 'getByHref').and.returnValue(observableOf({
requestUUIDs: ['request1', 'request2']
objectCache.getByHref.and.returnValue(observableOf({
requestUUIDs: ['request1', 'request2'],
dependentRequestUUIDs: [],
} as ObjectCacheEntry));
spyOn(dataService, 'invalidateByHref');
service.addMemberToGroup(GroupMock, EPersonMock2).subscribe();
@@ -206,8 +210,9 @@ describe('GroupDataService', () => {
describe('deleteMemberFromGroup', () => {
beforeEach(() => {
spyOn(objectCache, 'getByHref').and.returnValue(observableOf({
requestUUIDs: ['request1', 'request2']
objectCache.getByHref.and.returnValue(observableOf({
requestUUIDs: ['request1', 'request2'],
dependentRequestUUIDs: [],
} as ObjectCacheEntry));
spyOn(dataService, 'invalidateByHref');
service.deleteMemberFromGroup(GroupMock, EPersonMock).subscribe();

View File

@@ -13,6 +13,8 @@ export function getMockObjectCacheService(): ObjectCacheService {
'hasByUUID',
'hasByHref',
'getRequestUUIDBySelfLink',
'addDependency',
'removeDependents',
]);
}