fixed issue where resources that were fetched from a list where not able to be rendered separately

This commit is contained in:
Art Lowel
2017-06-19 15:20:48 +02:00
parent e37ea12e2d
commit f6550e2628
8 changed files with 100 additions and 57 deletions

View File

@@ -30,37 +30,33 @@ export class RemoteDataBuildService {
href: string, href: string,
normalizedType: GenericConstructor<TNormalized> normalizedType: GenericConstructor<TNormalized>
): RemoteData<TDomain> { ): RemoteData<TDomain> {
const requestObs = this.store.select<RequestEntry>('core', 'data', 'request', href); const requestHrefObs = this.objectCache.getRequestHrefBySelfLink(href);
const responseCacheObs = this.responseCache.get(href);
const requestPending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.requestPending).distinctUntilChanged(); const requestObs = Observable.race(
this.store.select<RequestEntry>('core', 'data', 'request', href).filter(entry => hasValue(entry)),
requestHrefObs.flatMap(requestHref =>
this.store.select<RequestEntry>('core', 'data', 'request', requestHref)).filter(entry => hasValue(entry))
);
const responsePending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.responsePending).distinctUntilChanged(); const responseCacheObs = Observable.race(
this.responseCache.get(href).filter(entry => hasValue(entry)),
requestHrefObs.flatMap(requestHref => this.responseCache.get(requestHref)).filter(entry => hasValue(entry))
);
const requestPending = requestObs.map((entry: RequestEntry) => entry.requestPending).distinctUntilChanged();
const responsePending = requestObs.map((entry: RequestEntry) => entry.responsePending).distinctUntilChanged();
const isSuccessFul = responseCacheObs const isSuccessFul = responseCacheObs
.map((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful).distinctUntilChanged(); .map((entry: ResponseCacheEntry) => entry.response.isSuccessful).distinctUntilChanged();
const errorMessage = responseCacheObs const errorMessage = responseCacheObs
.filter((entry: ResponseCacheEntry) => hasValue(entry) && !entry.response.isSuccessful) .filter((entry: ResponseCacheEntry) => !entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage) .map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
.distinctUntilChanged(); .distinctUntilChanged();
const payload = const payload = this.objectCache.getBySelfLink<TNormalized>(href, normalizedType)
Observable.race( .map((normalized: TNormalized) => {
this.objectCache.getBySelfLink<TNormalized>(href, normalizedType),
responseCacheObs
.filter((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
.flatMap((resourceUUIDs: Array<string>) => {
if (isNotEmpty(resourceUUIDs)) {
return this.objectCache.get(resourceUUIDs[0], normalizedType);
}
else {
return Observable.of(undefined);
}
})
.distinctUntilChanged()
).map((normalized: TNormalized) => {
return this.build<TNormalized, TDomain>(normalized); return this.build<TNormalized, TDomain>(normalized);
}); });
@@ -78,23 +74,24 @@ export class RemoteDataBuildService {
href: string, href: string,
normalizedType: GenericConstructor<TNormalized> normalizedType: GenericConstructor<TNormalized>
): RemoteData<TDomain[]> { ): RemoteData<TDomain[]> {
const requestObs = this.store.select<RequestEntry>('core', 'data', 'request', href); const requestObs = this.store.select<RequestEntry>('core', 'data', 'request', href)
const responseCacheObs = this.responseCache.get(href); .filter(entry => hasValue(entry));
const responseCacheObs = this.responseCache.get(href).filter(entry => hasValue(entry));
const requestPending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.requestPending).distinctUntilChanged(); const requestPending = requestObs.map((entry: RequestEntry) => entry.requestPending).distinctUntilChanged();
const responsePending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.responsePending).distinctUntilChanged(); const responsePending = requestObs.map((entry: RequestEntry) => entry.responsePending).distinctUntilChanged();
const isSuccessFul = responseCacheObs const isSuccessFul = responseCacheObs
.map((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful).distinctUntilChanged(); .map((entry: ResponseCacheEntry) => entry.response.isSuccessful).distinctUntilChanged();
const errorMessage = responseCacheObs const errorMessage = responseCacheObs
.filter((entry: ResponseCacheEntry) => hasValue(entry) && !entry.response.isSuccessful) .filter((entry: ResponseCacheEntry) => !entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage) .map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
.distinctUntilChanged(); .distinctUntilChanged();
const payload = responseCacheObs const payload = responseCacheObs
.filter((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful) .filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs) .map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
.flatMap((resourceUUIDs: Array<string>) => { .flatMap((resourceUUIDs: Array<string>) => {
return this.objectCache.getList(resourceUUIDs, normalizedType) return this.objectCache.getList(resourceUUIDs, normalizedType)

View File

@@ -20,6 +20,7 @@ export class AddToObjectCacheAction implements Action {
objectToCache: CacheableObject; objectToCache: CacheableObject;
timeAdded: number; timeAdded: number;
msToLive: number; msToLive: number;
requestHref: string;
}; };
/** /**
@@ -31,9 +32,13 @@ export class AddToObjectCacheAction implements Action {
* the time it was added * the time it was added
* @param msToLive * @param msToLive
* the amount of milliseconds before it should expire * the amount of milliseconds before it should expire
* @param requestHref
* The href 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
*/ */
constructor(objectToCache: CacheableObject, timeAdded: number, msToLive: number) { constructor(objectToCache: CacheableObject, timeAdded: number, msToLive: number, requestHref: string) {
this.payload = { objectToCache, timeAdded, msToLive }; this.payload = { objectToCache, timeAdded, msToLive, requestHref };
} }
} }

View File

@@ -24,7 +24,8 @@ describe("objectCacheReducer", () => {
foo: "bar" foo: "bar"
}, },
timeAdded: new Date().getTime(), timeAdded: new Date().getTime(),
msToLive: 900000 msToLive: 900000,
requestHref: "https://rest.api/endpoint/uuid1"
}, },
[uuid2]: { [uuid2]: {
data: { data: {
@@ -32,7 +33,8 @@ describe("objectCacheReducer", () => {
foo: "baz" foo: "baz"
}, },
timeAdded: new Date().getTime(), timeAdded: new Date().getTime(),
msToLive: 900000 msToLive: 900000,
requestHref: "https://rest.api/endpoint/uuid2"
} }
}; };
deepFreeze(testState); deepFreeze(testState);
@@ -56,7 +58,8 @@ describe("objectCacheReducer", () => {
const objectToCache = {uuid: uuid1}; const objectToCache = {uuid: uuid1};
const timeAdded = new Date().getTime(); const timeAdded = new Date().getTime();
const msToLive = 900000; const msToLive = 900000;
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive); const requestHref = "https://rest.api/endpoint/uuid1";
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
const newState = objectCacheReducer(state, action); const newState = objectCacheReducer(state, action);
expect(newState[uuid1].data).toEqual(objectToCache); expect(newState[uuid1].data).toEqual(objectToCache);
@@ -68,7 +71,8 @@ describe("objectCacheReducer", () => {
const objectToCache = {uuid: uuid1, foo: "baz", somethingElse: true}; const objectToCache = {uuid: uuid1, foo: "baz", somethingElse: true};
const timeAdded = new Date().getTime(); const timeAdded = new Date().getTime();
const msToLive = 900000; const msToLive = 900000;
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive); const requestHref = "https://rest.api/endpoint/uuid1";
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
const newState = objectCacheReducer(testState, action); const newState = objectCacheReducer(testState, action);
expect(newState[uuid1].data['foo']).toBe("baz"); expect(newState[uuid1].data['foo']).toBe("baz");
@@ -80,7 +84,8 @@ describe("objectCacheReducer", () => {
const objectToCache = {uuid: uuid1}; const objectToCache = {uuid: uuid1};
const timeAdded = new Date().getTime(); const timeAdded = new Date().getTime();
const msToLive = 900000; const msToLive = 900000;
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive); const requestHref = "https://rest.api/endpoint/uuid1";
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
deepFreeze(state); deepFreeze(state);
objectCacheReducer(state, action); objectCacheReducer(state, action);

View File

@@ -22,6 +22,7 @@ export class ObjectCacheEntry implements CacheEntry {
data: CacheableObject; data: CacheableObject;
timeAdded: number; timeAdded: number;
msToLive: number; msToLive: number;
requestHref: string;
} }
/** /**
@@ -83,7 +84,8 @@ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheActio
[action.payload.objectToCache.uuid]: { [action.payload.objectToCache.uuid]: {
data: action.payload.objectToCache, data: action.payload.objectToCache,
timeAdded: action.payload.timeAdded, timeAdded: action.payload.timeAdded,
msToLive: action.payload.msToLive msToLive: action.payload.msToLive,
requestHref: action.payload.requestHref
} }
}); });
} }

View File

@@ -22,9 +22,13 @@ export class ObjectCacheService {
* The object to add * The object to add
* @param msToLive * @param msToLive
* The number of milliseconds it should be cached for * The number of milliseconds it should be cached for
* @param requestHref
* The href 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
*/ */
add(objectToCache: CacheableObject, msToLive: number): void { add(objectToCache: CacheableObject, msToLive: number, requestHref: string): void {
this.store.dispatch(new AddToObjectCacheAction(objectToCache, new Date().getTime(), msToLive)); this.store.dispatch(new AddToObjectCacheAction(objectToCache, new Date().getTime(), msToLive, requestHref));
} }
/** /**
@@ -54,9 +58,7 @@ export class ObjectCacheService {
* An observable of the requested object * An observable of the requested object
*/ */
get<T extends CacheableObject>(uuid: string, type: GenericConstructor<T>): Observable<T> { get<T extends CacheableObject>(uuid: string, type: GenericConstructor<T>): Observable<T> {
return this.store.select<ObjectCacheEntry>('core', 'cache', 'object', uuid) return this.getEntry(uuid)
.filter(entry => this.isValid(entry))
.distinctUntilChanged()
.map((entry: ObjectCacheEntry) => <T> Object.assign(new type(), entry.data)); .map((entry: ObjectCacheEntry) => <T> Object.assign(new type(), entry.data));
} }
@@ -65,6 +67,23 @@ export class ObjectCacheService {
.flatMap((uuid: string) => this.get(uuid, type)) .flatMap((uuid: string) => this.get(uuid, type))
} }
private getEntry(uuid: string): Observable<ObjectCacheEntry> {
return this.store.select<ObjectCacheEntry>('core', 'cache', 'object', uuid)
.filter(entry => this.isValid(entry))
.distinctUntilChanged();
}
getRequestHref(uuid: string): Observable<string> {
return this.getEntry(uuid)
.map((entry: ObjectCacheEntry) => entry.requestHref)
.distinctUntilChanged();
}
getRequestHrefBySelfLink(self: string): Observable<string> {
return this.store.select<string>('core', 'index', 'href', self)
.flatMap((uuid: string) => this.getRequestHref(uuid));
}
/** /**
* Get an observable for an array of objects of the same type * Get an observable for an array of objects of the same type
* with the specified UUIDs * with the specified UUIDs

View File

@@ -45,7 +45,7 @@ export class RequestEffects {
}) })
.flatMap((entry: RequestEntry) => { .flatMap((entry: RequestEntry) => {
return this.restApi.get(entry.request.href) return this.restApi.get(entry.request.href)
.map((data: DSpaceRESTV2Response) => this.processEmbedded(data._embedded)) .map((data: DSpaceRESTV2Response) => this.processEmbedded(data._embedded, entry.request.href))
.map((ids: Array<string>) => new SuccessResponse(ids)) .map((ids: Array<string>) => new SuccessResponse(ids))
.do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive)) .do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
.map((response: Response) => new RequestCompleteAction(entry.request.href)) .map((response: Response) => new RequestCompleteAction(entry.request.href))
@@ -54,25 +54,25 @@ export class RequestEffects {
.map((response: Response) => new RequestCompleteAction(entry.request.href))); .map((response: Response) => new RequestCompleteAction(entry.request.href)));
}); });
protected processEmbedded(_embedded: any): Array<string> { protected processEmbedded(_embedded: any, requestHref): Array<string> {
if (isNotEmpty(_embedded)) { if (isNotEmpty(_embedded)) {
if (isObjectLevel(_embedded)) { if (isObjectLevel(_embedded)) {
return this.deserializeAndCache(_embedded); return this.deserializeAndCache(_embedded, requestHref);
} }
else { else {
let uuids = []; let uuids = [];
Object.keys(_embedded) Object.keys(_embedded)
.filter(property => _embedded.hasOwnProperty(property)) .filter(property => _embedded.hasOwnProperty(property))
.forEach(property => { .forEach(property => {
uuids = [...uuids, ...this.deserializeAndCache(_embedded[property])]; uuids = [...uuids, ...this.deserializeAndCache(_embedded[property], requestHref)];
}); });
return uuids; return uuids;
} }
} }
} }
protected deserializeAndCache(obj): Array<string> { protected deserializeAndCache(obj, requestHref): Array<string> {
let type: ResourceType; let type: ResourceType;
const isArray = Array.isArray(obj); const isArray = Array.isArray(obj);
@@ -96,19 +96,19 @@ export class RequestEffects {
if (isArray) { if (isArray) {
obj.forEach(o => { obj.forEach(o => {
if (isNotEmpty(o._embedded)) { if (isNotEmpty(o._embedded)) {
this.processEmbedded(o._embedded); this.processEmbedded(o._embedded, requestHref);
} }
}); });
const normalizedObjArr = serializer.deserializeArray(obj); const normalizedObjArr = serializer.deserializeArray(obj);
normalizedObjArr.forEach(t => this.addToObjectCache(t)); normalizedObjArr.forEach(t => this.addToObjectCache(t, requestHref));
return normalizedObjArr.map(t => t.uuid); return normalizedObjArr.map(t => t.uuid);
} }
else { else {
if (isNotEmpty(obj._embedded)) { if (isNotEmpty(obj._embedded)) {
this.processEmbedded(obj._embedded); this.processEmbedded(obj._embedded, requestHref);
} }
const normalizedObj = serializer.deserialize(obj); const normalizedObj = serializer.deserialize(obj);
this.addToObjectCache(normalizedObj); this.addToObjectCache(normalizedObj, requestHref);
return [normalizedObj.uuid]; return [normalizedObj.uuid];
} }
@@ -125,10 +125,10 @@ export class RequestEffects {
} }
} }
protected addToObjectCache(co: CacheableObject): void { protected addToObjectCache(co: CacheableObject, requestHref: string): void {
if (hasNoValue(co) || hasNoValue(co.uuid)) { if (hasNoValue(co) || hasNoValue(co.uuid)) {
throw new Error('The server returned an invalid object'); throw new Error('The server returned an invalid object');
} }
this.objectCache.add(co, this.EnvConfig.cache.msToLive); this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref);
} }
} }

View File

@@ -8,6 +8,9 @@ import { RequestConfigureAction, RequestExecuteAction } from "./request.actions"
import { ResponseCacheService } from "../cache/response-cache.service"; import { ResponseCacheService } from "../cache/response-cache.service";
import { ObjectCacheService } from "../cache/object-cache.service"; import { ObjectCacheService } from "../cache/object-cache.service";
import { CacheableObject } from "../cache/object-cache.reducer"; import { CacheableObject } from "../cache/object-cache.reducer";
import { ResponseCacheEntry } from "../cache/response-cache.reducer";
import { request } from "http";
import { SuccessResponse } from "../cache/response-cache.models";
@Injectable() @Injectable()
export class RequestService { export class RequestService {
@@ -35,7 +38,19 @@ export class RequestService {
} }
configure<T extends CacheableObject>(request: Request<T>): void { configure<T extends CacheableObject>(request: Request<T>): void {
const isCached = this.objectCache.hasBySelfLink(request.href); let isCached = this.objectCache.hasBySelfLink(request.href);
if (!isCached && this.responseCache.has(request.href)) {
//if it isn't cached it may be a list endpoint, if so verify
//every object included in the response is still cached
this.responseCache.get(request.href)
.take(1)
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
.map((resourceUUIDs: Array<string>) => resourceUUIDs.every(uuid => this.objectCache.has(uuid)))
.subscribe(c => isCached = c);
}
const isPending = this.isPending(request.href); const isPending = this.isPending(request.href);
if (!(isCached || isPending)) { if (!(isCached || isPending)) {

View File

@@ -35,7 +35,7 @@ export const COMMUNITIES = {
], ],
"_links": { "_links": {
"self": { "self": {
"href": "http://dspace7.4science.it/dspace-spring-rest/api/core/community/9076bd16-e69a-48d6-9e41-0238cb40d863" "href": "/communities/6631"
}, },
"collections": [ "collections": [
{ "href": "/collections/5179" } { "href": "/collections/5179" }
@@ -78,7 +78,7 @@ export const COMMUNITIES = {
], ],
"_links": { "_links": {
"self": { "self": {
"href": "http://dspace7.4science.it/dspace-spring-rest/api/core/community/9076bd16-e69a-48d6-9e41-0238cb40d863" "href": "/communities/2365"
}, },
"collections": [ "collections": [
{ "href": "/collections/6547" } { "href": "/collections/6547" }