mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
switched to self links as keys in the object cache
This commit is contained in:
@@ -70,10 +70,10 @@ export class RemoteDataBuildService {
|
||||
this.objectCache.getBySelfLink<TNormalized>(href, normalizedType).startWith(undefined),
|
||||
responseCacheObs
|
||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
||||
.map((entry: ResponseCacheEntry) => (entry.response as SuccessResponse).resourceUUIDs)
|
||||
.flatMap((resourceUUIDs: string[]) => {
|
||||
if (isNotEmpty(resourceUUIDs)) {
|
||||
return this.objectCache.get(resourceUUIDs[0], normalizedType);
|
||||
.map((entry: ResponseCacheEntry) => (entry.response as SuccessResponse).resourceSelfLinks)
|
||||
.flatMap((resourceSelfLinks: string[]) => {
|
||||
if (isNotEmpty(resourceSelfLinks)) {
|
||||
return this.objectCache.getBySelfLink(resourceSelfLinks[0], normalizedType);
|
||||
} else {
|
||||
return Observable.of(undefined);
|
||||
}
|
||||
@@ -137,7 +137,7 @@ export class RemoteDataBuildService {
|
||||
|
||||
const payload = responseCacheObs
|
||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
||||
.map((entry: ResponseCacheEntry) => (entry.response as SuccessResponse).resourceUUIDs)
|
||||
.map((entry: ResponseCacheEntry) => (entry.response as SuccessResponse).resourceSelfLinks)
|
||||
.flatMap((resourceUUIDs: string[]) => {
|
||||
return this.objectCache.getList(resourceUUIDs, normalizedType)
|
||||
.map((normList: TNormalized[]) => {
|
||||
|
46
src/app/core/cache/object-cache.reducer.spec.ts
vendored
46
src/app/core/cache/object-cache.reducer.spec.ts
vendored
@@ -16,26 +16,26 @@ class NullAction extends RemoveFromObjectCacheAction {
|
||||
}
|
||||
|
||||
describe('objectCacheReducer', () => {
|
||||
const uuid1 = '1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||
const uuid2 = '28b04544-1766-4e82-9728-c4e93544ecd3';
|
||||
const selfLink1 = 'https://localhost:8080/api/core/items/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||
const selfLink2 = 'https://localhost:8080/api/core/items/28b04544-1766-4e82-9728-c4e93544ecd3';
|
||||
const testState = {
|
||||
[uuid1]: {
|
||||
[selfLink1]: {
|
||||
data: {
|
||||
uuid: uuid1,
|
||||
self: selfLink1,
|
||||
foo: 'bar'
|
||||
},
|
||||
timeAdded: new Date().getTime(),
|
||||
msToLive: 900000,
|
||||
requestHref: 'https://rest.api/endpoint/uuid1'
|
||||
requestHref: selfLink1
|
||||
},
|
||||
[uuid2]: {
|
||||
[selfLink2]: {
|
||||
data: {
|
||||
uuid: uuid2,
|
||||
self: selfLink2,
|
||||
foo: 'baz'
|
||||
},
|
||||
timeAdded: new Date().getTime(),
|
||||
msToLive: 900000,
|
||||
requestHref: 'https://rest.api/endpoint/uuid2'
|
||||
requestHref: selfLink2
|
||||
}
|
||||
};
|
||||
deepFreeze(testState);
|
||||
@@ -56,38 +56,38 @@ describe('objectCacheReducer', () => {
|
||||
|
||||
it('should add the payload to the cache in response to an ADD action', () => {
|
||||
const state = Object.create(null);
|
||||
const objectToCache = { uuid: uuid1 };
|
||||
const objectToCache = { self: selfLink1 };
|
||||
const timeAdded = new Date().getTime();
|
||||
const msToLive = 900000;
|
||||
const requestHref = 'https://rest.api/endpoint/uuid1';
|
||||
const requestHref = 'https://rest.api/endpoint/selfLink1';
|
||||
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
|
||||
const newState = objectCacheReducer(state, action);
|
||||
|
||||
expect(newState[uuid1].data).toEqual(objectToCache);
|
||||
expect(newState[uuid1].timeAdded).toEqual(timeAdded);
|
||||
expect(newState[uuid1].msToLive).toEqual(msToLive);
|
||||
expect(newState[selfLink1].data).toEqual(objectToCache);
|
||||
expect(newState[selfLink1].timeAdded).toEqual(timeAdded);
|
||||
expect(newState[selfLink1].msToLive).toEqual(msToLive);
|
||||
});
|
||||
|
||||
it('should overwrite an object in the cache in response to an ADD action if it already exists', () => {
|
||||
const objectToCache = { uuid: uuid1, foo: 'baz', somethingElse: true };
|
||||
const objectToCache = { self: selfLink1, foo: 'baz', somethingElse: true };
|
||||
const timeAdded = new Date().getTime();
|
||||
const msToLive = 900000;
|
||||
const requestHref = 'https://rest.api/endpoint/uuid1';
|
||||
const requestHref = 'https://rest.api/endpoint/selfLink1';
|
||||
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
|
||||
const newState = objectCacheReducer(testState, action);
|
||||
|
||||
/* tslint:disable:no-string-literal */
|
||||
expect(newState[uuid1].data['foo']).toBe('baz');
|
||||
expect(newState[uuid1].data['somethingElse']).toBe(true);
|
||||
expect(newState[selfLink1].data['foo']).toBe('baz');
|
||||
expect(newState[selfLink1].data['somethingElse']).toBe(true);
|
||||
/* tslint:enable:no-string-literal */
|
||||
});
|
||||
|
||||
it('should perform the ADD action without affecting the previous state', () => {
|
||||
const state = Object.create(null);
|
||||
const objectToCache = { uuid: uuid1 };
|
||||
const objectToCache = { self: selfLink1 };
|
||||
const timeAdded = new Date().getTime();
|
||||
const msToLive = 900000;
|
||||
const requestHref = 'https://rest.api/endpoint/uuid1';
|
||||
const requestHref = 'https://rest.api/endpoint/selfLink1';
|
||||
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
|
||||
deepFreeze(state);
|
||||
|
||||
@@ -95,11 +95,11 @@ describe('objectCacheReducer', () => {
|
||||
});
|
||||
|
||||
it('should remove the specified object from the cache in response to the REMOVE action', () => {
|
||||
const action = new RemoveFromObjectCacheAction(uuid1);
|
||||
const action = new RemoveFromObjectCacheAction(selfLink1);
|
||||
const newState = objectCacheReducer(testState, action);
|
||||
|
||||
expect(testState[uuid1]).not.toBeUndefined();
|
||||
expect(newState[uuid1]).toBeUndefined();
|
||||
expect(testState[selfLink1]).not.toBeUndefined();
|
||||
expect(newState[selfLink1]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("shouldn't do anything in response to the REMOVE action for an object that isn't cached", () => {
|
||||
@@ -112,7 +112,7 @@ describe('objectCacheReducer', () => {
|
||||
});
|
||||
|
||||
it('should perform the REMOVE action without affecting the previous state', () => {
|
||||
const action = new RemoveFromObjectCacheAction(uuid1);
|
||||
const action = new RemoveFromObjectCacheAction(selfLink1);
|
||||
// testState has already been frozen above
|
||||
objectCacheReducer(testState, action);
|
||||
});
|
||||
|
12
src/app/core/cache/object-cache.reducer.ts
vendored
12
src/app/core/cache/object-cache.reducer.ts
vendored
@@ -8,11 +8,11 @@ import { CacheEntry } from './cache-entry';
|
||||
/**
|
||||
* An interface to represent objects that can be cached
|
||||
*
|
||||
* A cacheable object should have a uuid
|
||||
* A cacheable object should have a self link
|
||||
*/
|
||||
export interface CacheableObject {
|
||||
uuid: string;
|
||||
self?: string;
|
||||
uuid?: string;
|
||||
self: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,11 +28,11 @@ export class ObjectCacheEntry implements CacheEntry {
|
||||
/**
|
||||
* The ObjectCache State
|
||||
*
|
||||
* Consists of a map with UUIDs as keys,
|
||||
* Consists of a map with self links as keys,
|
||||
* and ObjectCacheEntries as values
|
||||
*/
|
||||
export interface ObjectCacheState {
|
||||
[uuid: string]: ObjectCacheEntry
|
||||
[href: string]: ObjectCacheEntry
|
||||
}
|
||||
|
||||
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
||||
@@ -81,7 +81,7 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi
|
||||
*/
|
||||
function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState {
|
||||
return Object.assign({}, state, {
|
||||
[action.payload.objectToCache.uuid]: {
|
||||
[action.payload.objectToCache.self]: {
|
||||
data: action.payload.objectToCache,
|
||||
timeAdded: action.payload.timeAdded,
|
||||
msToLive: action.payload.msToLive,
|
||||
|
57
src/app/core/cache/object-cache.service.spec.ts
vendored
57
src/app/core/cache/object-cache.service.spec.ts
vendored
@@ -8,12 +8,12 @@ import { CoreState } from '../core.reducers';
|
||||
|
||||
class TestClass implements CacheableObject {
|
||||
constructor(
|
||||
public uuid: string,
|
||||
public self: string,
|
||||
public foo: string
|
||||
) { }
|
||||
|
||||
test(): string {
|
||||
return this.foo + this.uuid;
|
||||
return this.foo + this.self;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,11 @@ describe('ObjectCacheService', () => {
|
||||
let service: ObjectCacheService;
|
||||
let store: Store<CoreState>;
|
||||
|
||||
const uuid = '1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||
const requestHref = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||
const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||
const timestamp = new Date().getTime();
|
||||
const msToLive = 900000;
|
||||
const objectToCache = {
|
||||
uuid: uuid,
|
||||
self: selfLink,
|
||||
foo: 'bar'
|
||||
};
|
||||
const cacheEntry = {
|
||||
@@ -48,73 +47,73 @@ describe('ObjectCacheService', () => {
|
||||
|
||||
describe('add', () => {
|
||||
it('should dispatch an ADD action with the object to add, the time to live, and the current timestamp', () => {
|
||||
service.add(objectToCache, msToLive, requestHref);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, requestHref));
|
||||
service.add(objectToCache, msToLive, selfLink);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, selfLink));
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should dispatch a REMOVE action with the UUID of the object to remove', () => {
|
||||
service.remove(uuid);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new RemoveFromObjectCacheAction(uuid));
|
||||
it('should dispatch a REMOVE action with the self link of the object to remove', () => {
|
||||
service.remove(selfLink);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new RemoveFromObjectCacheAction(selfLink));
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should return an observable of the cached object with the specified UUID and type', () => {
|
||||
describe('getBySelfLink', () => {
|
||||
it('should return an observable of the cached object with the specified self link and type', () => {
|
||||
spyOn(store, 'select').and.returnValue(Observable.of(cacheEntry));
|
||||
|
||||
let testObj: any;
|
||||
// due to the implementation of spyOn above, this subscribe will be synchronous
|
||||
service.get(uuid, TestClass).take(1).subscribe((o) => testObj = o);
|
||||
expect(testObj.uuid).toBe(uuid);
|
||||
service.getBySelfLink(selfLink, TestClass).take(1).subscribe((o) => testObj = o);
|
||||
expect(testObj.self).toBe(selfLink);
|
||||
expect(testObj.foo).toBe('bar');
|
||||
// this only works if testObj is an instance of TestClass
|
||||
expect(testObj.test()).toBe('bar' + uuid);
|
||||
expect(testObj.test()).toBe('bar' + selfLink);
|
||||
});
|
||||
|
||||
it('should not return a cached object that has exceeded its time to live', () => {
|
||||
spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry));
|
||||
|
||||
let getObsHasFired = false;
|
||||
const subscription = service.get(uuid, TestClass).subscribe((o) => getObsHasFired = true);
|
||||
const subscription = service.getBySelfLink(selfLink, TestClass).subscribe((o) => getObsHasFired = true);
|
||||
expect(getObsHasFired).toBe(false);
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getList', () => {
|
||||
it('should return an observable of the array of cached objects with the specified UUID and type', () => {
|
||||
spyOn(service, 'get').and.returnValue(Observable.of(new TestClass(uuid, 'bar')));
|
||||
it('should return an observable of the array of cached objects with the specified self link and type', () => {
|
||||
spyOn(service, 'getBySelfLink').and.returnValue(Observable.of(new TestClass(selfLink, 'bar')));
|
||||
|
||||
let testObjs: any[];
|
||||
service.getList([uuid, uuid], TestClass).take(1).subscribe((arr) => testObjs = arr);
|
||||
expect(testObjs[0].uuid).toBe(uuid);
|
||||
service.getList([selfLink, selfLink], TestClass).take(1).subscribe((arr) => testObjs = arr);
|
||||
expect(testObjs[0].self).toBe(selfLink);
|
||||
expect(testObjs[0].foo).toBe('bar');
|
||||
expect(testObjs[0].test()).toBe('bar' + uuid);
|
||||
expect(testObjs[1].uuid).toBe(uuid);
|
||||
expect(testObjs[0].test()).toBe('bar' + selfLink);
|
||||
expect(testObjs[1].self).toBe(selfLink);
|
||||
expect(testObjs[1].foo).toBe('bar');
|
||||
expect(testObjs[1].test()).toBe('bar' + uuid);
|
||||
expect(testObjs[1].test()).toBe('bar' + selfLink);
|
||||
});
|
||||
});
|
||||
|
||||
describe('has', () => {
|
||||
it('should return true if the object with the supplied UUID is cached and still valid', () => {
|
||||
it('should return true if the object with the supplied self link is cached and still valid', () => {
|
||||
spyOn(store, 'select').and.returnValue(Observable.of(cacheEntry));
|
||||
|
||||
expect(service.has(uuid)).toBe(true);
|
||||
expect(service.hasBySelfLink(selfLink)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the object with the supplied UUID isn't cached", () => {
|
||||
it("should return false if the object with the supplied self link isn't cached", () => {
|
||||
spyOn(store, 'select').and.returnValue(Observable.of(undefined));
|
||||
|
||||
expect(service.has(uuid)).toBe(false);
|
||||
expect(service.hasBySelfLink(selfLink)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if the object with the supplied UUID is cached but has exceeded its time to live', () => {
|
||||
it('should return false if the object with the supplied self link is cached but has exceeded its time to live', () => {
|
||||
spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry));
|
||||
|
||||
expect(service.has(uuid)).toBe(false);
|
||||
expect(service.hasBySelfLink(selfLink)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
68
src/app/core/cache/object-cache.service.ts
vendored
68
src/app/core/cache/object-cache.service.ts
vendored
@@ -10,12 +10,12 @@ import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { keySelector } from '../shared/selectors';
|
||||
|
||||
function objectFromUuidSelector(uuid: string): MemoizedSelector<CoreState, ObjectCacheEntry> {
|
||||
return keySelector<ObjectCacheEntry>('data/object', uuid);
|
||||
function selfLinkFromUuidSelector(uuid: string): MemoizedSelector<CoreState, string> {
|
||||
return keySelector<string>('index/uuid', uuid);
|
||||
}
|
||||
|
||||
function uuidFromHrefSelector(href: string): MemoizedSelector<CoreState, string> {
|
||||
return keySelector<string>('index/href', href);
|
||||
function entryFromSelfLinkSelector(selfLink: string): MemoizedSelector<CoreState, ObjectCacheEntry> {
|
||||
return keySelector<ObjectCacheEntry>('data/object', selfLink);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,7 +35,7 @@ export class ObjectCacheService {
|
||||
* @param msToLive
|
||||
* The number of milliseconds it should be cached for
|
||||
* @param requestHref
|
||||
* The href of the request that resulted in this object
|
||||
* The selfLink of the request that resulted in this object
|
||||
* This isn't necessarily the same as the object's self
|
||||
* link, it could have been part of a list for example
|
||||
*/
|
||||
@@ -69,55 +69,55 @@ export class ObjectCacheService {
|
||||
* @return Observable<T>
|
||||
* An observable of the requested object
|
||||
*/
|
||||
get<T extends CacheableObject>(uuid: string, type: GenericConstructor<T>): Observable<T> {
|
||||
return this.getEntry(uuid)
|
||||
getByUUID<T extends CacheableObject>(uuid: string, type: GenericConstructor<T>): Observable<T> {
|
||||
return this.store.select(selfLinkFromUuidSelector(uuid))
|
||||
.flatMap((selfLink: string) => this.getBySelfLink(selfLink, type))
|
||||
}
|
||||
|
||||
getBySelfLink<T extends CacheableObject>(selfLink: string, type: GenericConstructor<T>): Observable<T> {
|
||||
return this.getEntry(selfLink)
|
||||
.map((entry: ObjectCacheEntry) => Object.assign(new type(), entry.data) as T);
|
||||
}
|
||||
|
||||
getBySelfLink<T extends CacheableObject>(href: string, type: GenericConstructor<T>): Observable<T> {
|
||||
return this.store.select(uuidFromHrefSelector(href))
|
||||
.flatMap((uuid: string) => this.get(uuid, type))
|
||||
}
|
||||
|
||||
private getEntry(uuid: string): Observable<ObjectCacheEntry> {
|
||||
return this.store.select(objectFromUuidSelector(uuid))
|
||||
private getEntry(selfLink: string): Observable<ObjectCacheEntry> {
|
||||
return this.store.select(entryFromSelfLinkSelector(selfLink))
|
||||
.filter((entry) => this.isValid(entry))
|
||||
.distinctUntilChanged();
|
||||
}
|
||||
|
||||
getRequestHref(uuid: string): Observable<string> {
|
||||
return this.getEntry(uuid)
|
||||
getRequestHrefBySelfLink(selfLink: string): Observable<string> {
|
||||
return this.getEntry(selfLink)
|
||||
.map((entry: ObjectCacheEntry) => entry.requestHref)
|
||||
.distinctUntilChanged();
|
||||
}
|
||||
|
||||
getRequestHrefBySelfLink(self: string): Observable<string> {
|
||||
return this.store.select(uuidFromHrefSelector(self))
|
||||
.flatMap((uuid: string) => this.getRequestHref(uuid));
|
||||
getRequestHrefByUUID(uuid: string): Observable<string> {
|
||||
return this.store.select(selfLinkFromUuidSelector(uuid))
|
||||
.flatMap((selfLink: string) => this.getRequestHrefBySelfLink(selfLink));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an observable for an array of objects of the same type
|
||||
* with the specified UUIDs
|
||||
* with the specified self links
|
||||
*
|
||||
* The type needs to be specified as well, in order to turn
|
||||
* the cached plain javascript object in to an instance of
|
||||
* a class.
|
||||
*
|
||||
* e.g. getList([
|
||||
* 'c96588c6-72d3-425d-9d47-fa896255a695',
|
||||
* 'cff860da-cf5f-4fda-b8c9-afb7ec0b2d9e'
|
||||
* 'http://localhost:8080/api/core/collections/c96588c6-72d3-425d-9d47-fa896255a695',
|
||||
* 'http://localhost:8080/api/core/collections/cff860da-cf5f-4fda-b8c9-afb7ec0b2d9e'
|
||||
* ], Collection)
|
||||
*
|
||||
* @param uuids
|
||||
* An array of UUIDs of the objects to get
|
||||
* @param selfLinks
|
||||
* An array of self links of the objects to get
|
||||
* @param type
|
||||
* The type of the objects to get
|
||||
* @return Observable<Array<T>>
|
||||
*/
|
||||
getList<T extends CacheableObject>(uuids: string[], type: GenericConstructor<T>): Observable<T[]> {
|
||||
getList<T extends CacheableObject>(selfLinks: string[], type: GenericConstructor<T>): Observable<T[]> {
|
||||
return Observable.combineLatest(
|
||||
uuids.map((id: string) => this.get<T>(id, type))
|
||||
selfLinks.map((selfLink: string) => this.getBySelfLink<T>(selfLink, type))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -130,12 +130,12 @@ export class ObjectCacheService {
|
||||
* true if the object with the specified UUID is cached,
|
||||
* false otherwise
|
||||
*/
|
||||
has(uuid: string): boolean {
|
||||
hasByUUID(uuid: string): boolean {
|
||||
let result: boolean;
|
||||
|
||||
this.store.select(objectFromUuidSelector(uuid))
|
||||
this.store.select(selfLinkFromUuidSelector(uuid))
|
||||
.take(1)
|
||||
.subscribe((entry) => result = this.isValid(entry));
|
||||
.subscribe((selfLink: string) => result = this.hasBySelfLink(selfLink));
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -143,18 +143,18 @@ export class ObjectCacheService {
|
||||
/**
|
||||
* Check whether the object with the specified self link is cached
|
||||
*
|
||||
* @param href
|
||||
* @param selfLink
|
||||
* The self link of the object to check
|
||||
* @return boolean
|
||||
* true if the object with the specified self link is cached,
|
||||
* false otherwise
|
||||
*/
|
||||
hasBySelfLink(href: string): boolean {
|
||||
hasBySelfLink(selfLink: string): boolean {
|
||||
let result = false;
|
||||
|
||||
this.store.select(uuidFromHrefSelector(href))
|
||||
this.store.select(entryFromSelfLinkSelector(selfLink))
|
||||
.take(1)
|
||||
.subscribe((uuid: string) => result = this.has(uuid));
|
||||
.subscribe((entry: ObjectCacheEntry) => result = this.isValid(entry));
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -175,7 +175,7 @@ export class ObjectCacheService {
|
||||
const timeOutdated = entry.timeAdded + entry.msToLive;
|
||||
const isOutDated = new Date().getTime() > timeOutdated;
|
||||
if (isOutDated) {
|
||||
this.store.dispatch(new RemoveFromObjectCacheAction(entry.data.uuid));
|
||||
this.store.dispatch(new RemoveFromObjectCacheAction(entry.data.self));
|
||||
}
|
||||
return !isOutDated;
|
||||
}
|
||||
|
2
src/app/core/cache/response-cache.models.ts
vendored
2
src/app/core/cache/response-cache.models.ts
vendored
@@ -11,7 +11,7 @@ export class Response {
|
||||
|
||||
export class SuccessResponse extends Response {
|
||||
constructor(
|
||||
public resourceUUIDs: string[],
|
||||
public resourceSelfLinks: string[],
|
||||
public statusCode: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
|
@@ -1,12 +1,12 @@
|
||||
|
||||
import { ObjectCacheEffects } from './data/object-cache.effects';
|
||||
import { RequestCacheEffects } from './data/request-cache.effects';
|
||||
import { HrefIndexEffects } from './index/href-index.effects';
|
||||
import { UUIDIndexEffects } from './index/uuid-index.effects';
|
||||
import { RequestEffects } from './data/request.effects';
|
||||
|
||||
export const coreEffects = [
|
||||
RequestCacheEffects,
|
||||
RequestEffects,
|
||||
ObjectCacheEffects,
|
||||
HrefIndexEffects,
|
||||
UUIDIndexEffects,
|
||||
];
|
||||
|
@@ -2,21 +2,21 @@ import { ActionReducerMap, createFeatureSelector } from '@ngrx/store';
|
||||
|
||||
import { responseCacheReducer, ResponseCacheState } from './cache/response-cache.reducer';
|
||||
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
|
||||
import { hrefIndexReducer, HrefIndexState } from './index/href-index.reducer';
|
||||
import { uuidIndexReducer, UUIDIndexState } from './index/uuid-index.reducer';
|
||||
import { requestReducer, RequestState } from './data/request.reducer';
|
||||
|
||||
export interface CoreState {
|
||||
'data/object': ObjectCacheState,
|
||||
'data/response': ResponseCacheState,
|
||||
'data/request': RequestState,
|
||||
'index/href': HrefIndexState
|
||||
'index/uuid': UUIDIndexState
|
||||
}
|
||||
|
||||
export const coreReducers: ActionReducerMap<CoreState> = {
|
||||
'data/object': objectCacheReducer,
|
||||
'data/response': responseCacheReducer,
|
||||
'data/request': requestReducer,
|
||||
'index/href': hrefIndexReducer
|
||||
'index/uuid': uuidIndexReducer
|
||||
};
|
||||
|
||||
export const coreSelector = createFeatureSelector<CoreState>('core');
|
||||
|
@@ -5,14 +5,13 @@ import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { FindAllOptions, FindAllRequest, FindByIDRequest, Request } from './request.models';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { RequestConfigureAction, RequestExecuteAction } from './request.actions';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { Inject } from '@angular/core';
|
||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||
import { GlobalConfig } from '../../../config';
|
||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
export abstract class DataService<TNormalized extends CacheableObject, TDomain> {
|
||||
protected abstract objectCache: ObjectCacheService;
|
||||
@@ -30,6 +29,13 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
|
||||
|
||||
}
|
||||
|
||||
private getEndpoint(linkName: string): Observable<string> {
|
||||
const apiUrl = new RESTURLCombiner(this.EnvConfig, '/').toString();
|
||||
this.requestService.configure(new Request(apiUrl));
|
||||
// TODO fetch from store
|
||||
return Observable.of(undefined);
|
||||
}
|
||||
|
||||
protected getFindAllHref(options: FindAllOptions = {}): string {
|
||||
let result;
|
||||
const args = [];
|
||||
|
@@ -15,10 +15,10 @@ export const RequestActionTypes = {
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
export class RequestConfigureAction implements Action {
|
||||
type = RequestActionTypes.CONFIGURE;
|
||||
payload: Request<CacheableObject>;
|
||||
payload: Request;
|
||||
|
||||
constructor(
|
||||
request: Request<CacheableObject>
|
||||
request: Request
|
||||
) {
|
||||
this.payload = request;
|
||||
}
|
||||
|
@@ -57,8 +57,8 @@ export class RequestEffects {
|
||||
return this.restApi.get(entry.request.href)
|
||||
.map((data: DSpaceRESTV2Response) => {
|
||||
const processRequestDTO = this.process(data.payload, entry.request.href);
|
||||
const uuids = flattenSingleKeyObject(processRequestDTO).map((no) => no.uuid);
|
||||
return new SuccessResponse(uuids, data.statusCode, this.processPageInfo(data.payload.page))
|
||||
const selfLinks = flattenSingleKeyObject(processRequestDTO).map((no) => no.self);
|
||||
return new SuccessResponse(selfLinks, data.statusCode, this.processPageInfo(data.payload.page))
|
||||
}).do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
|
||||
.map((response: Response) => new RequestCompleteAction(entry.request.href))
|
||||
.catch((error: RequestError) => Observable.of(new ErrorResponse(error))
|
||||
@@ -157,7 +157,7 @@ export class RequestEffects {
|
||||
}
|
||||
|
||||
protected addToObjectCache(co: CacheableObject, requestHref: string): void {
|
||||
if (hasNoValue(co) || hasNoValue(co.uuid)) {
|
||||
if (hasNoValue(co) || hasNoValue(co.self)) {
|
||||
throw new Error('The server returned an invalid object');
|
||||
}
|
||||
this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref);
|
||||
|
@@ -3,13 +3,13 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
export class Request<T> {
|
||||
export class Request {
|
||||
constructor(
|
||||
public href: string,
|
||||
) { }
|
||||
}
|
||||
|
||||
export class FindByIDRequest<T> extends Request<T> {
|
||||
export class FindByIDRequest extends Request {
|
||||
constructor(
|
||||
href: string,
|
||||
public resourceID: string
|
||||
@@ -25,7 +25,7 @@ export class FindAllOptions {
|
||||
sort?: SortOptions;
|
||||
}
|
||||
|
||||
export class FindAllRequest<T> extends Request<T> {
|
||||
export class FindAllRequest extends Request {
|
||||
constructor(
|
||||
href: string,
|
||||
public options?: FindAllOptions,
|
||||
|
@@ -6,7 +6,7 @@ import {
|
||||
import { Request } from './request.models';
|
||||
|
||||
export class RequestEntry {
|
||||
request: Request<CacheableObject>;
|
||||
request: Request;
|
||||
requestPending: boolean;
|
||||
responsePending: boolean;
|
||||
completed: boolean;
|
||||
|
@@ -45,7 +45,7 @@ export class RequestService {
|
||||
return this.store.select(entryFromHrefSelector(href));
|
||||
}
|
||||
|
||||
configure<T extends CacheableObject>(request: Request<T>): void {
|
||||
configure<T extends CacheableObject>(request: Request): void {
|
||||
let isCached = this.objectCache.hasBySelfLink(request.href);
|
||||
|
||||
if (!isCached && this.responseCache.has(request.href)) {
|
||||
@@ -54,8 +54,8 @@ export class RequestService {
|
||||
this.responseCache.get(request.href)
|
||||
.take(1)
|
||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
||||
.map((entry: ResponseCacheEntry) => (entry.response as SuccessResponse).resourceUUIDs)
|
||||
.map((resourceUUIDs: string[]) => resourceUUIDs.every((uuid) => this.objectCache.has(uuid)))
|
||||
.map((entry: ResponseCacheEntry) => (entry.response as SuccessResponse).resourceSelfLinks)
|
||||
.map((resourceSelfLinks: string[]) => resourceSelfLinks.every((selfLink) => this.objectCache.hasBySelfLink(selfLink)))
|
||||
.subscribe((c) => isCached = c);
|
||||
}
|
||||
|
||||
|
@@ -1,60 +0,0 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
|
||||
import { type } from '../../shared/ngrx/type';
|
||||
|
||||
/**
|
||||
* The list of HrefIndexAction type definitions
|
||||
*/
|
||||
export const HrefIndexActionTypes = {
|
||||
ADD: type('dspace/core/index/href/ADD'),
|
||||
REMOVE_UUID: type('dspace/core/index/href/REMOVE_UUID')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
* An ngrx action to add an href to the index
|
||||
*/
|
||||
export class AddToHrefIndexAction implements Action {
|
||||
type = HrefIndexActionTypes.ADD;
|
||||
payload: {
|
||||
href: string;
|
||||
uuid: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new AddToHrefIndexAction
|
||||
*
|
||||
* @param href
|
||||
* the href to add
|
||||
* @param uuid
|
||||
* the uuid of the resource the href links to
|
||||
*/
|
||||
constructor(href: string, uuid: string) {
|
||||
this.payload = { href, uuid };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to remove an href from the index
|
||||
*/
|
||||
export class RemoveUUIDFromHrefIndexAction implements Action {
|
||||
type = HrefIndexActionTypes.REMOVE_UUID;
|
||||
payload: string;
|
||||
|
||||
/**
|
||||
* Create a new RemoveUUIDFromHrefIndexAction
|
||||
*
|
||||
* @param uuid
|
||||
* the uuid to remove all hrefs for
|
||||
*/
|
||||
constructor(uuid: string) {
|
||||
this.payload = uuid;
|
||||
}
|
||||
|
||||
}
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* A type to encompass all HrefIndexActions
|
||||
*/
|
||||
export type HrefIndexAction = AddToHrefIndexAction | RemoveUUIDFromHrefIndexAction;
|
@@ -1,46 +0,0 @@
|
||||
import {
|
||||
HrefIndexAction,
|
||||
HrefIndexActionTypes,
|
||||
AddToHrefIndexAction,
|
||||
RemoveUUIDFromHrefIndexAction
|
||||
} from './href-index.actions';
|
||||
|
||||
export interface HrefIndexState {
|
||||
[href: string]: string
|
||||
}
|
||||
|
||||
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
||||
const initialState: HrefIndexState = Object.create(null);
|
||||
|
||||
export function hrefIndexReducer(state = initialState, action: HrefIndexAction): HrefIndexState {
|
||||
switch (action.type) {
|
||||
|
||||
case HrefIndexActionTypes.ADD: {
|
||||
return addToHrefIndex(state, action as AddToHrefIndexAction);
|
||||
}
|
||||
|
||||
case HrefIndexActionTypes.REMOVE_UUID: {
|
||||
return removeUUIDFromHrefIndex(state, action as RemoveUUIDFromHrefIndexAction)
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addToHrefIndex(state: HrefIndexState, action: AddToHrefIndexAction): HrefIndexState {
|
||||
return Object.assign({}, state, {
|
||||
[action.payload.href]: action.payload.uuid
|
||||
});
|
||||
}
|
||||
|
||||
function removeUUIDFromHrefIndex(state: HrefIndexState, action: RemoveUUIDFromHrefIndexAction): HrefIndexState {
|
||||
const newState = Object.create(null);
|
||||
for (const href in state) {
|
||||
if (state[href] !== action.payload) {
|
||||
newState[href] = state[href];
|
||||
}
|
||||
}
|
||||
return newState;
|
||||
}
|
60
src/app/core/index/uuid-index.actions.ts
Normal file
60
src/app/core/index/uuid-index.actions.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
|
||||
import { type } from '../../shared/ngrx/type';
|
||||
|
||||
/**
|
||||
* The list of HrefIndexAction type definitions
|
||||
*/
|
||||
export const UUIDIndexActionTypes = {
|
||||
ADD: type('dspace/core/index/uuid/ADD'),
|
||||
REMOVE_HREF: type('dspace/core/index/uuid/REMOVE_HREF')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
* An ngrx action to add an href to the index
|
||||
*/
|
||||
export class AddToUUIDIndexAction implements Action {
|
||||
type = UUIDIndexActionTypes.ADD;
|
||||
payload: {
|
||||
href: string;
|
||||
uuid: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new AddToUUIDIndexAction
|
||||
*
|
||||
* @param uuid
|
||||
* the uuid to add
|
||||
* @param href
|
||||
* the self link of the resource the uuid belongs to
|
||||
*/
|
||||
constructor(uuid: string, href: string) {
|
||||
this.payload = { href, uuid };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to remove an href from the index
|
||||
*/
|
||||
export class RemoveHrefFromUUIDIndexAction implements Action {
|
||||
type = UUIDIndexActionTypes.REMOVE_HREF;
|
||||
payload: string;
|
||||
|
||||
/**
|
||||
* Create a new RemoveHrefFromUUIDIndexAction
|
||||
*
|
||||
* @param href
|
||||
* the href to remove the UUID for
|
||||
*/
|
||||
constructor(href: string) {
|
||||
this.payload = href;
|
||||
}
|
||||
|
||||
}
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* A type to encompass all HrefIndexActions
|
||||
*/
|
||||
export type UUIDIndexAction = AddToUUIDIndexAction | RemoveHrefFromUUIDIndexAction;
|
@@ -5,26 +5,26 @@ import {
|
||||
ObjectCacheActionTypes, AddToObjectCacheAction,
|
||||
RemoveFromObjectCacheAction
|
||||
} from '../cache/object-cache.actions';
|
||||
import { AddToHrefIndexAction, RemoveUUIDFromHrefIndexAction } from './href-index.actions';
|
||||
import { AddToUUIDIndexAction, RemoveHrefFromUUIDIndexAction } from './uuid-index.actions';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
|
||||
@Injectable()
|
||||
export class HrefIndexEffects {
|
||||
export class UUIDIndexEffects {
|
||||
|
||||
@Effect() add$ = this.actions$
|
||||
.ofType(ObjectCacheActionTypes.ADD)
|
||||
.filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.self))
|
||||
.filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.uuid))
|
||||
.map((action: AddToObjectCacheAction) => {
|
||||
return new AddToHrefIndexAction(
|
||||
action.payload.objectToCache.self,
|
||||
action.payload.objectToCache.uuid
|
||||
return new AddToUUIDIndexAction(
|
||||
action.payload.objectToCache.uuid,
|
||||
action.payload.objectToCache.self
|
||||
);
|
||||
});
|
||||
|
||||
@Effect() remove$ = this.actions$
|
||||
.ofType(ObjectCacheActionTypes.REMOVE)
|
||||
.map((action: RemoveFromObjectCacheAction) => {
|
||||
return new RemoveUUIDFromHrefIndexAction(action.payload);
|
||||
return new RemoveHrefFromUUIDIndexAction(action.payload);
|
||||
});
|
||||
|
||||
constructor(private actions$: Actions) {
|
46
src/app/core/index/uuid-index.reducer.ts
Normal file
46
src/app/core/index/uuid-index.reducer.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
UUIDIndexAction,
|
||||
UUIDIndexActionTypes,
|
||||
AddToUUIDIndexAction,
|
||||
RemoveHrefFromUUIDIndexAction
|
||||
} from './uuid-index.actions';
|
||||
|
||||
export interface UUIDIndexState {
|
||||
[uuid: string]: string
|
||||
}
|
||||
|
||||
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
||||
const initialState: UUIDIndexState = Object.create(null);
|
||||
|
||||
export function uuidIndexReducer(state = initialState, action: UUIDIndexAction): UUIDIndexState {
|
||||
switch (action.type) {
|
||||
|
||||
case UUIDIndexActionTypes.ADD: {
|
||||
return addToUUIDIndex(state, action as AddToUUIDIndexAction);
|
||||
}
|
||||
|
||||
case UUIDIndexActionTypes.REMOVE_HREF: {
|
||||
return removeHrefFromUUIDIndex(state, action as RemoveHrefFromUUIDIndexAction)
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addToUUIDIndex(state: UUIDIndexState, action: AddToUUIDIndexAction): UUIDIndexState {
|
||||
return Object.assign({}, state, {
|
||||
[action.payload.uuid]: action.payload.href
|
||||
});
|
||||
}
|
||||
|
||||
function removeHrefFromUUIDIndex(state: UUIDIndexState, action: RemoveHrefFromUUIDIndexAction): UUIDIndexState {
|
||||
const newState = Object.create(null);
|
||||
for (const uuid in state) {
|
||||
if (state[uuid] !== action.payload) {
|
||||
newState[uuid] = state[uuid];
|
||||
}
|
||||
}
|
||||
return newState;
|
||||
}
|
Reference in New Issue
Block a user