requestCacheReducer docs & tests

This commit is contained in:
Art Lowel
2017-03-08 18:30:55 +01:00
parent a535b86e0b
commit eaaf12d645
5 changed files with 426 additions and 1 deletions

View File

@@ -2,12 +2,18 @@ import { Action } from "@ngrx/store";
import { type } from "../../shared/ngrx/type"; import { type } from "../../shared/ngrx/type";
import { CacheableObject } from "./object-cache.reducer"; import { CacheableObject } from "./object-cache.reducer";
/**
* The list of ObjectCacheAction type definitions
*/
export const ObjectCacheActionTypes = { export const ObjectCacheActionTypes = {
ADD: type('dspace/core/cache/object/ADD'), ADD: type('dspace/core/cache/object/ADD'),
REMOVE: type('dspace/core/cache/object/REMOVE'), REMOVE: type('dspace/core/cache/object/REMOVE'),
RESET_TIMESTAMPS: type('dspace/core/cache/object/RESET_TIMESTAMPS') RESET_TIMESTAMPS: type('dspace/core/cache/object/RESET_TIMESTAMPS')
}; };
/**
* An ngrx action to add an object to the cache
*/
export class AddToObjectCacheAction implements Action { export class AddToObjectCacheAction implements Action {
type = ObjectCacheActionTypes.ADD; type = ObjectCacheActionTypes.ADD;
payload: { payload: {
@@ -16,29 +22,60 @@ export class AddToObjectCacheAction implements Action {
msToLive: number; 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) { constructor(objectToCache: CacheableObject, timeAdded: number, msToLive: number) {
this.payload = { objectToCache, timeAdded, msToLive }; this.payload = { objectToCache, timeAdded, msToLive };
} }
} }
/**
* An ngrx action to remove an object from the cache
*/
export class RemoveFromObjectCacheAction implements Action { export class RemoveFromObjectCacheAction implements Action {
type = ObjectCacheActionTypes.REMOVE; type = ObjectCacheActionTypes.REMOVE;
payload: string; payload: string;
/**
* Create a new RemoveFromObjectCacheAction
*
* @param uuid
* the UUID of the object to remove
*/
constructor(uuid: string) { constructor(uuid: string) {
this.payload = uuid; this.payload = uuid;
} }
} }
/**
* An ngrx action to reset the timeAdded property of all cached objects
*/
export class ResetObjectCacheTimestampsAction implements Action { export class ResetObjectCacheTimestampsAction implements Action {
type = ObjectCacheActionTypes.RESET_TIMESTAMPS; type = ObjectCacheActionTypes.RESET_TIMESTAMPS;
payload: number; payload: number;
/**
* Create a new ResetObjectCacheTimestampsAction
*
* @param newTimestamp
* the new timeAdded all objects should get
*/
constructor(newTimestamp: number) { constructor(newTimestamp: number) {
this.payload = newTimestamp; this.payload = newTimestamp;
} }
} }
/**
* A type to encompass all ObjectCacheActions
*/
export type ObjectCacheAction export type ObjectCacheAction
= AddToObjectCacheAction = AddToObjectCacheAction
| RemoveFromObjectCacheAction | RemoveFromObjectCacheAction

View File

@@ -95,9 +95,11 @@ describe("objectCacheReducer", () => {
}); });
it("shouldn't do anything in response to the REMOVE action for an object that isn't cached", () => { 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); const newState = objectCacheReducer(testState, action);
expect(testState[wrongKey]).toBeUndefined();
expect(newState).toEqual(testState); expect(newState).toEqual(testState);
}); });

View File

@@ -4,6 +4,9 @@ import { type } from "../../shared/ngrx/type";
import { PaginationOptions } from "../shared/pagination-options.model"; import { PaginationOptions } from "../shared/pagination-options.model";
import { SortOptions } from "../shared/sort-options.model"; import { SortOptions } from "../shared/sort-options.model";
/**
* The list of RequestCacheAction type definitions
*/
export const RequestCacheActionTypes = { export const RequestCacheActionTypes = {
FIND_BY_ID: type('dspace/core/cache/request/FIND_BY_ID'), FIND_BY_ID: type('dspace/core/cache/request/FIND_BY_ID'),
FIND_ALL: type('dspace/core/cache/request/FIND_ALL'), 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') 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 { export class RequestCacheFindAllAction implements Action {
type = RequestCacheActionTypes.FIND_ALL; type = RequestCacheActionTypes.FIND_ALL;
payload: { payload: {
@@ -23,6 +29,20 @@ export class RequestCacheFindAllAction implements Action {
sortOptions: SortOptions 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( constructor(
key: string, key: string,
service: OpaqueToken, service: OpaqueToken,
@@ -40,6 +60,9 @@ export class RequestCacheFindAllAction implements Action {
} }
} }
/**
* An ngrx action to find objects by id
*/
export class RequestCacheFindByIDAction implements Action { export class RequestCacheFindByIDAction implements Action {
type = RequestCacheActionTypes.FIND_BY_ID; type = RequestCacheActionTypes.FIND_BY_ID;
payload: { payload: {
@@ -48,6 +71,16 @@ export class RequestCacheFindByIDAction implements Action {
resourceID: string 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( constructor(
key: string, key: string,
service: OpaqueToken, 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 { export class RequestCacheSuccessAction implements Action {
type = RequestCacheActionTypes.SUCCESS; type = RequestCacheActionTypes.SUCCESS;
payload: { payload: {
@@ -70,6 +106,20 @@ export class RequestCacheSuccessAction implements Action {
msToLive: number 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<string>, timeAdded, msToLive: number) { constructor(key: string, resourceUUIDs: Array<string>, timeAdded, msToLive: number) {
this.payload = { this.payload = {
key, key,
@@ -80,6 +130,9 @@ export class RequestCacheSuccessAction implements Action {
} }
} }
/**
* An ngrx action to indicate a request failed
*/
export class RequestCacheErrorAction implements Action { export class RequestCacheErrorAction implements Action {
type = RequestCacheActionTypes.ERROR; type = RequestCacheActionTypes.ERROR;
payload: { payload: {
@@ -87,6 +140,16 @@ export class RequestCacheErrorAction implements Action {
errorMessage: string 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) { constructor(key: string, errorMessage: string) {
this.payload = { this.payload = {
key, 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 { export class RequestCacheRemoveAction implements Action {
type = RequestCacheActionTypes.REMOVE; type = RequestCacheActionTypes.REMOVE;
payload: string; payload: string;
/**
* Create a new RequestCacheRemoveAction
* @param key
* The key of the request to remove
*/
constructor(key: string) { constructor(key: string) {
this.payload = key; this.payload = key;
} }
} }
/**
* An ngrx action to reset the timeAdded property of all cached objects
*/
export class ResetRequestCacheTimestampsAction implements Action { export class ResetRequestCacheTimestampsAction implements Action {
type = RequestCacheActionTypes.RESET_TIMESTAMPS; type = RequestCacheActionTypes.RESET_TIMESTAMPS;
payload: number; payload: number;
/**
* Create a new ResetObjectCacheTimestampsAction
*
* @param newTimestamp
* the new timeAdded all objects should get
*/
constructor(newTimestamp: number) { constructor(newTimestamp: number) {
this.payload = newTimestamp; this.payload = newTimestamp;
} }
} }
/**
* A type to encompass all RequestCacheActions
*/
export type RequestCacheAction export type RequestCacheAction
= RequestCacheFindAllAction = RequestCacheFindAllAction
| RequestCacheFindByIDAction | RequestCacheFindByIDAction

View File

@@ -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);
});
});
});
});

View File

@@ -9,6 +9,9 @@ import { OpaqueToken } from "@angular/core";
import { CacheEntry } from "./cache-entry"; import { CacheEntry } from "./cache-entry";
import { hasValue } from "../../shared/empty.util"; import { hasValue } from "../../shared/empty.util";
/**
* An entry in the RequestCache
*/
export class RequestCacheEntry implements CacheEntry { export class RequestCacheEntry implements CacheEntry {
service: OpaqueToken; service: OpaqueToken;
key: string; key: string;
@@ -24,6 +27,9 @@ export class RequestCacheEntry implements CacheEntry {
msToLive: number; msToLive: number;
} }
/**
* The RequestCache State
*/
export interface RequestCacheState { export interface RequestCacheState {
[key: string]: RequestCacheEntry [key: string]: RequestCacheEntry
} }
@@ -31,6 +37,16 @@ export interface RequestCacheState {
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) // Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
const initialState = Object.create(null); 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 => { export const requestCacheReducer = (state = initialState, action: RequestCacheAction): RequestCacheState => {
switch (action.type) { 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 { function findAllRequest(state: RequestCacheState, action: RequestCacheFindAllAction): RequestCacheState {
return Object.assign({}, state, { return Object.assign({}, state, {
[action.payload.key]: { [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 { function findByIDRequest(state: RequestCacheState, action: RequestCacheFindByIDAction): RequestCacheState {
return Object.assign({}, state, { return Object.assign({}, state, {
[action.payload.key]: { [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 { function success(state: RequestCacheState, action: RequestCacheSuccessAction): RequestCacheState {
return Object.assign({}, state, { return Object.assign({}, state, {
[action.payload.key]: Object.assign({}, state[action.payload.key], { [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 { function error(state: RequestCacheState, action: RequestCacheErrorAction): RequestCacheState {
return Object.assign({}, state, { return Object.assign({}, state, {
[action.payload.key]: Object.assign({}, state[action.payload.key], { [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 { function removeFromCache(state: RequestCacheState, action: RequestCacheRemoveAction): RequestCacheState {
if (hasValue(state[action.payload])) { if (hasValue(state[action.payload])) {
let newCache = Object.assign({}, state); 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 { function resetRequestCacheTimestamps(state: RequestCacheState, action: ResetRequestCacheTimestampsAction): RequestCacheState {
let newState = Object.create(null); let newState = Object.create(null);
Object.keys(state).forEach(key => { Object.keys(state).forEach(key => {