diff --git a/src/app/core/cache/object-cache.actions.ts b/src/app/core/cache/object-cache.actions.ts index 93cf247024..cc9e557de4 100644 --- a/src/app/core/cache/object-cache.actions.ts +++ b/src/app/core/cache/object-cache.actions.ts @@ -2,12 +2,18 @@ import { Action } from "@ngrx/store"; import { type } from "../../shared/ngrx/type"; import { CacheableObject } from "./object-cache.reducer"; +/** + * The list of ObjectCacheAction type definitions + */ export const ObjectCacheActionTypes = { ADD: type('dspace/core/cache/object/ADD'), REMOVE: type('dspace/core/cache/object/REMOVE'), RESET_TIMESTAMPS: type('dspace/core/cache/object/RESET_TIMESTAMPS') }; +/** + * An ngrx action to add an object to the cache + */ export class AddToObjectCacheAction implements Action { type = ObjectCacheActionTypes.ADD; payload: { @@ -16,29 +22,60 @@ export class AddToObjectCacheAction implements Action { msToLive: number; }; + /** + * Create a new AddToObjectCacheAction + * + * @param objectToCache + * the object to add + * @param timeAdded + * the time it was added + * @param msToLive + * the amount of milliseconds before it should expire + */ constructor(objectToCache: CacheableObject, timeAdded: number, msToLive: number) { this.payload = { objectToCache, timeAdded, msToLive }; } } +/** + * An ngrx action to remove an object from the cache + */ export class RemoveFromObjectCacheAction implements Action { type = ObjectCacheActionTypes.REMOVE; payload: string; + /** + * Create a new RemoveFromObjectCacheAction + * + * @param uuid + * the UUID of the object to remove + */ constructor(uuid: string) { this.payload = uuid; } } +/** + * An ngrx action to reset the timeAdded property of all cached objects + */ export class ResetObjectCacheTimestampsAction implements Action { type = ObjectCacheActionTypes.RESET_TIMESTAMPS; payload: number; + /** + * Create a new ResetObjectCacheTimestampsAction + * + * @param newTimestamp + * the new timeAdded all objects should get + */ constructor(newTimestamp: number) { this.payload = newTimestamp; } } +/** + * A type to encompass all ObjectCacheActions + */ export type ObjectCacheAction = AddToObjectCacheAction | RemoveFromObjectCacheAction diff --git a/src/app/core/cache/object-cache.reducer.spec.ts b/src/app/core/cache/object-cache.reducer.spec.ts index eb9422d93a..4d8e116e4f 100644 --- a/src/app/core/cache/object-cache.reducer.spec.ts +++ b/src/app/core/cache/object-cache.reducer.spec.ts @@ -95,9 +95,11 @@ describe("objectCacheReducer", () => { }); it("shouldn't do anything in response to the REMOVE action for an object that isn't cached", () => { - const action = new RemoveFromObjectCacheAction("this isn't cached"); + const wrongKey = "this isn't cached"; + const action = new RemoveFromObjectCacheAction(wrongKey); const newState = objectCacheReducer(testState, action); + expect(testState[wrongKey]).toBeUndefined(); expect(newState).toEqual(testState); }); diff --git a/src/app/core/cache/request-cache.actions.ts b/src/app/core/cache/request-cache.actions.ts index a5d7e73ef4..78c6692d71 100644 --- a/src/app/core/cache/request-cache.actions.ts +++ b/src/app/core/cache/request-cache.actions.ts @@ -4,6 +4,9 @@ import { type } from "../../shared/ngrx/type"; import { PaginationOptions } from "../shared/pagination-options.model"; import { SortOptions } from "../shared/sort-options.model"; +/** + * The list of RequestCacheAction type definitions + */ export const RequestCacheActionTypes = { FIND_BY_ID: type('dspace/core/cache/request/FIND_BY_ID'), FIND_ALL: type('dspace/core/cache/request/FIND_ALL'), @@ -13,6 +16,9 @@ export const RequestCacheActionTypes = { RESET_TIMESTAMPS: type('dspace/core/cache/request/RESET_TIMESTAMPS') }; +/** + * An ngrx action to find all objects of a certain type + */ export class RequestCacheFindAllAction implements Action { type = RequestCacheActionTypes.FIND_ALL; payload: { @@ -23,6 +29,20 @@ export class RequestCacheFindAllAction implements Action { sortOptions: SortOptions }; + /** + * Create a new RequestCacheFindAllAction + * + * @param key + * the key under which to cache this request, should be unique + * @param service + * the name of the service that initiated the action + * @param scopeID + * the id of an optional scope object + * @param paginationOptions + * the pagination options + * @param sortOptions + * the sort options + */ constructor( key: string, service: OpaqueToken, @@ -40,6 +60,9 @@ export class RequestCacheFindAllAction implements Action { } } +/** + * An ngrx action to find objects by id + */ export class RequestCacheFindByIDAction implements Action { type = RequestCacheActionTypes.FIND_BY_ID; payload: { @@ -48,6 +71,16 @@ export class RequestCacheFindByIDAction implements Action { resourceID: string }; + /** + * Create a new RequestCacheFindByIDAction + * + * @param key + * the key under which to cache this request, should be unique + * @param service + * the name of the service that initiated the action + * @param resourceID + * the ID of the resource to find + */ constructor( key: string, service: OpaqueToken, @@ -61,6 +94,9 @@ export class RequestCacheFindByIDAction implements Action { } } +/** + * An ngrx action to indicate a request was returned successful + */ export class RequestCacheSuccessAction implements Action { type = RequestCacheActionTypes.SUCCESS; payload: { @@ -70,6 +106,20 @@ export class RequestCacheSuccessAction implements Action { msToLive: number }; + /** + * Create a new RequestCacheSuccessAction + * + * @param key + * the key under which cache this request is cached, + * should be identical to the one used in the corresponding + * find action + * @param resourceUUIDs + * the UUIDs returned from the backend + * @param timeAdded + * the time it was returned + * @param msToLive + * the amount of milliseconds before it should expire + */ constructor(key: string, resourceUUIDs: Array, timeAdded, msToLive: number) { this.payload = { key, @@ -80,6 +130,9 @@ export class RequestCacheSuccessAction implements Action { } } +/** + * An ngrx action to indicate a request failed + */ export class RequestCacheErrorAction implements Action { type = RequestCacheActionTypes.ERROR; payload: { @@ -87,6 +140,16 @@ export class RequestCacheErrorAction implements Action { errorMessage: string }; + /** + * Create a new RequestCacheErrorAction + * + * @param key + * the key under which cache this request is cached, + * should be identical to the one used in the corresponding + * find action + * @param errorMessage + * A message describing the reason the request failed + */ constructor(key: string, errorMessage: string) { this.payload = { key, @@ -95,24 +158,44 @@ export class RequestCacheErrorAction implements Action { } } +/** + * An ngrx action to remove a request from the cache + */ export class RequestCacheRemoveAction implements Action { type = RequestCacheActionTypes.REMOVE; payload: string; + /** + * Create a new RequestCacheRemoveAction + * @param key + * The key of the request to remove + */ constructor(key: string) { this.payload = key; } } +/** + * An ngrx action to reset the timeAdded property of all cached objects + */ export class ResetRequestCacheTimestampsAction implements Action { type = RequestCacheActionTypes.RESET_TIMESTAMPS; payload: number; + /** + * Create a new ResetObjectCacheTimestampsAction + * + * @param newTimestamp + * the new timeAdded all objects should get + */ constructor(newTimestamp: number) { this.payload = newTimestamp; } } +/** + * A type to encompass all RequestCacheActions + */ export type RequestCacheAction = RequestCacheFindAllAction | RequestCacheFindByIDAction diff --git a/src/app/core/cache/request-cache.reducer.spec.ts b/src/app/core/cache/request-cache.reducer.spec.ts new file mode 100644 index 0000000000..56bea6a83e --- /dev/null +++ b/src/app/core/cache/request-cache.reducer.spec.ts @@ -0,0 +1,227 @@ +import { requestCacheReducer, RequestCacheState } from "./request-cache.reducer"; +import { + RequestCacheRemoveAction, RequestCacheFindByIDAction, + RequestCacheFindAllAction, RequestCacheSuccessAction, RequestCacheErrorAction, + ResetRequestCacheTimestampsAction +} from "./request-cache.actions"; +import deepFreeze = require("deep-freeze"); +import { OpaqueToken } from "@angular/core"; + +class NullAction extends RequestCacheRemoveAction { + type = null; + payload = null; + + constructor() { + super(null); + } +} + +describe("requestCacheReducer", () => { + const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"]; + const services = [new OpaqueToken('service1'), new OpaqueToken('service2')]; + const msToLive = 900000; + const uuids = [ + "9e32a2e2-6b91-4236-a361-995ccdc14c60", + "598ce822-c357-46f3-ab70-63724d02d6ad", + "be8325f7-243b-49f4-8a4b-df2b793ff3b5" + ]; + const resourceID = "9978"; + const paginationOptions = { "resultsPerPage": 10, "currentPage": 1 }; + const sortOptions = { "field": "id", "direction": 0 }; + const testState = { + [keys[0]]: { + "key": keys[0], + "service": services[0], + "resourceUUIDs": [uuids[0], uuids[1]], + "isLoading": false, + "paginationOptions": paginationOptions, + "sortOptions": sortOptions, + "timeAdded": new Date().getTime(), + "msToLive": msToLive + }, + [keys[1]]: { + "key": keys[1], + "service": services[1], + "resourceID": resourceID, + "resourceUUIDs": [uuids[2]], + "isLoading": false, + "timeAdded": new Date().getTime(), + "msToLive": msToLive + } + }; + deepFreeze(testState); + const errorState: {} = { + [keys[0]]: { + errorMessage: 'error', + resourceUUIDs: uuids + } + }; + deepFreeze(errorState); + + + it("should return the current state when no valid actions have been made", () => { + const action = new NullAction(); + const newState = requestCacheReducer(testState, action); + + expect(newState).toEqual(testState); + }); + + it("should start with an empty cache", () => { + const action = new NullAction(); + const initialState = requestCacheReducer(undefined, action); + + expect(initialState).toEqual(Object.create(null)); + }); + + describe("FIND_BY_ID", () => { + const action = new RequestCacheFindByIDAction(keys[0], services[0], resourceID); + + it("should perform the action without affecting the previous state", () => { + //testState has already been frozen above + requestCacheReducer(testState, action); + }); + + it("should add the request to the cache", () => { + const state = Object.create(null); + const newState = requestCacheReducer(state, action); + expect(newState[keys[0]].key).toBe(keys[0]); + expect(newState[keys[0]].service).toEqual(services[0]); + expect(newState[keys[0]].resourceID).toBe(resourceID); + }); + + it("should set isLoading to true", () => { + const state = Object.create(null); + const newState = requestCacheReducer(state, action); + expect(newState[keys[0]].isLoading).toBe(true); + }); + + it("should remove any previous error message or resourceUUID for the request", () => { + const newState = requestCacheReducer(errorState, action); + expect(newState[keys[0]].resourceUUIDs.length).toBe(0); + expect(newState[keys[0]].errorMessage).toBeUndefined(); + }); + }); + + describe("FIND_ALL", () => { + const action = new RequestCacheFindAllAction(keys[0], services[0], resourceID, paginationOptions, sortOptions); + + it("should perform the action without affecting the previous state", () => { + //testState has already been frozen above + requestCacheReducer(testState, action); + }); + + it("should add the request to the cache", () => { + const state = Object.create(null); + const newState = requestCacheReducer(state, action); + expect(newState[keys[0]].key).toBe(keys[0]); + expect(newState[keys[0]].service).toEqual(services[0]); + expect(newState[keys[0]].scopeID).toBe(resourceID); + expect(newState[keys[0]].paginationOptions).toEqual(paginationOptions); + expect(newState[keys[0]].sortOptions).toEqual(sortOptions); + }); + + it("should set isLoading to true", () => { + const state = Object.create(null); + const newState = requestCacheReducer(state, action); + expect(newState[keys[0]].isLoading).toBe(true); + }); + + it("should remove any previous error message or resourceUUIDs for the request", () => { + const newState = requestCacheReducer(errorState, action); + expect(newState[keys[0]].resourceUUIDs.length).toBe(0); + expect(newState[keys[0]].errorMessage).toBeUndefined(); + }); + }); + + describe("SUCCESS", () => { + const successUUIDs = [uuids[0], uuids[2]]; + const successTimeAdded = new Date().getTime(); + const successMsToLive = 5; + const action = new RequestCacheSuccessAction(keys[0], successUUIDs, successTimeAdded, successMsToLive); + + it("should perform the action without affecting the previous state", () => { + //testState has already been frozen above + requestCacheReducer(testState, action); + }); + + it("should add the response to the cached request", () => { + const newState = requestCacheReducer(testState, action); + expect(newState[keys[0]].resourceUUIDs).toBe(successUUIDs); + expect(newState[keys[0]].timeAdded).toBe(successTimeAdded); + expect(newState[keys[0]].msToLive).toBe(successMsToLive); + }); + + it("should set isLoading to false", () => { + const newState = requestCacheReducer(testState, action); + expect(newState[keys[0]].isLoading).toBe(false); + }); + + it("should remove any previous error message for the request", () => { + const newState = requestCacheReducer(errorState, action); + expect(newState[keys[0]].errorMessage).toBeUndefined(); + }); + }); + + describe("ERROR", () => { + const errorMsg = 'errorMsg'; + const action = new RequestCacheErrorAction(keys[0], errorMsg); + + it("should perform the action without affecting the previous state", () => { + //testState has already been frozen above + requestCacheReducer(testState, action); + }); + + it("should set an error message for the request", () => { + const newState = requestCacheReducer(errorState, action); + expect(newState[keys[0]].errorMessage).toBe(errorMsg); + }); + + it("should set isLoading to false", () => { + const newState = requestCacheReducer(testState, action); + expect(newState[keys[0]].isLoading).toBe(false); + }); + }); + + describe("REMOVE", () => { + it("should perform the action without affecting the previous state", () => { + const action = new RequestCacheRemoveAction(keys[0]); + //testState has already been frozen above + requestCacheReducer(testState, action); + }); + + it("should remove the specified request from the cache", () => { + const action = new RequestCacheRemoveAction(keys[0]); + const newState = requestCacheReducer(testState, action); + expect(testState[keys[0]]).not.toBeUndefined(); + expect(newState[keys[0]]).toBeUndefined(); + }); + + it("shouldn't do anything when the specified key isn't cached", () => { + const wrongKey = "this isn't cached"; + const action = new RequestCacheRemoveAction(wrongKey); + const newState = requestCacheReducer(testState, action); + expect(testState[wrongKey]).toBeUndefined(); + expect(newState).toEqual(testState); + }); + }); + + describe("RESET_TIMESTAMPS", () => { + const newTimeStamp = new Date().getTime(); + const action = new ResetRequestCacheTimestampsAction(newTimeStamp); + + it("should perform the action without affecting the previous state", () => { + //testState has already been frozen above + requestCacheReducer(testState, action); + }); + + it("should set the timestamp of all requests in the cache", () => { + const newState = requestCacheReducer(testState, action); + Object.keys(newState).forEach((key) => { + expect(newState[key].timeAdded).toEqual(newTimeStamp); + }); + }); + + }); + + +}); diff --git a/src/app/core/cache/request-cache.reducer.ts b/src/app/core/cache/request-cache.reducer.ts index 340b023548..0aa2e8c920 100644 --- a/src/app/core/cache/request-cache.reducer.ts +++ b/src/app/core/cache/request-cache.reducer.ts @@ -9,6 +9,9 @@ import { OpaqueToken } from "@angular/core"; import { CacheEntry } from "./cache-entry"; import { hasValue } from "../../shared/empty.util"; +/** + * An entry in the RequestCache + */ export class RequestCacheEntry implements CacheEntry { service: OpaqueToken; key: string; @@ -24,6 +27,9 @@ export class RequestCacheEntry implements CacheEntry { msToLive: number; } +/** + * The RequestCache State + */ export interface RequestCacheState { [key: string]: RequestCacheEntry } @@ -31,6 +37,16 @@ export interface RequestCacheState { // Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) const initialState = Object.create(null); +/** + * The RequestCache Reducer + * + * @param state + * the current state + * @param action + * the action to perform on the state + * @return RequestCacheState + * the new state + */ export const requestCacheReducer = (state = initialState, action: RequestCacheAction): RequestCacheState => { switch (action.type) { @@ -64,6 +80,16 @@ export const requestCacheReducer = (state = initialState, action: RequestCacheAc } }; +/** + * Add a FindAll request to the cache + * + * @param state + * the current state + * @param action + * a RequestCacheFindAllAction + * @return RequestCacheState + * the new state, with the request added, or overwritten + */ function findAllRequest(state: RequestCacheState, action: RequestCacheFindAllAction): RequestCacheState { return Object.assign({}, state, { [action.payload.key]: { @@ -79,6 +105,16 @@ function findAllRequest(state: RequestCacheState, action: RequestCacheFindAllAct }); } +/** + * Add a FindByID request to the cache + * + * @param state + * the current state + * @param action + * a RequestCacheFindByIDAction + * @return RequestCacheState + * the new state, with the request added, or overwritten + */ function findByIDRequest(state: RequestCacheState, action: RequestCacheFindByIDAction): RequestCacheState { return Object.assign({}, state, { [action.payload.key]: { @@ -92,6 +128,16 @@ function findByIDRequest(state: RequestCacheState, action: RequestCacheFindByIDA }); } +/** + * Update a cached request with a successful response + * + * @param state + * the current state + * @param action + * a RequestCacheSuccessAction + * @return RequestCacheState + * the new state, with the response added to the request + */ function success(state: RequestCacheState, action: RequestCacheSuccessAction): RequestCacheState { return Object.assign({}, state, { [action.payload.key]: Object.assign({}, state[action.payload.key], { @@ -104,6 +150,16 @@ function success(state: RequestCacheState, action: RequestCacheSuccessAction): R }); } +/** + * Update a cached request with an error + * + * @param state + * the current state + * @param action + * a RequestCacheSuccessAction + * @return RequestCacheState + * the new state, with the error added to the request + */ function error(state: RequestCacheState, action: RequestCacheErrorAction): RequestCacheState { return Object.assign({}, state, { [action.payload.key]: Object.assign({}, state[action.payload.key], { @@ -113,6 +169,16 @@ function error(state: RequestCacheState, action: RequestCacheErrorAction): Reque }); } +/** + * Remove a request from the cache + * + * @param state + * the current state + * @param action + * an RequestCacheRemoveAction + * @return RequestCacheState + * the new state, with the request removed if it existed. + */ function removeFromCache(state: RequestCacheState, action: RequestCacheRemoveAction): RequestCacheState { if (hasValue(state[action.payload])) { let newCache = Object.assign({}, state); @@ -125,6 +191,16 @@ function removeFromCache(state: RequestCacheState, action: RequestCacheRemoveAct } } +/** + * Set the timeAdded timestamp of every cached request to the specified value + * + * @param state + * the current state + * @param action + * a ResetRequestCacheTimestampsAction + * @return RequestCacheState + * the new state, with all timeAdded timestamps set to the specified value + */ function resetRequestCacheTimestamps(state: RequestCacheState, action: ResetRequestCacheTimestampsAction): RequestCacheState { let newState = Object.create(null); Object.keys(state).forEach(key => {