diff --git a/src/app/core/cache/object-cache.actions.ts b/src/app/core/cache/object-cache.actions.ts index 21855e5170..93cf247024 100644 --- a/src/app/core/cache/object-cache.actions.ts +++ b/src/app/core/cache/object-cache.actions.ts @@ -4,7 +4,8 @@ import { CacheableObject } from "./object-cache.reducer"; export const ObjectCacheActionTypes = { 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') }; export class AddToObjectCacheAction implements Action { @@ -29,6 +30,16 @@ export class RemoveFromObjectCacheAction implements Action { } } +export class ResetObjectCacheTimestampsAction implements Action { + type = ObjectCacheActionTypes.RESET_TIMESTAMPS; + payload: number; + + constructor(newTimestamp: number) { + this.payload = newTimestamp; + } +} + export type ObjectCacheAction = AddToObjectCacheAction | RemoveFromObjectCacheAction + | ResetObjectCacheTimestampsAction; diff --git a/src/app/core/cache/object-cache.reducer.spec.ts b/src/app/core/cache/object-cache.reducer.spec.ts index dbc0c20eed..eb9422d93a 100644 --- a/src/app/core/cache/object-cache.reducer.spec.ts +++ b/src/app/core/cache/object-cache.reducer.spec.ts @@ -2,7 +2,7 @@ import * as deepFreeze from "deep-freeze"; import { objectCacheReducer } from "./object-cache.reducer"; import { AddToObjectCacheAction, - RemoveFromObjectCacheAction + RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction } from "./object-cache.actions"; class NullAction extends RemoveFromObjectCacheAction { @@ -15,15 +15,24 @@ class NullAction extends RemoveFromObjectCacheAction { } describe("objectCacheReducer", () => { - const uuid = '1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; + const uuid1 = '1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; + const uuid2 = '28b04544-1766-4e82-9728-c4e93544ecd3'; const testState = { - [uuid]: { + [uuid1]: { data: { - uuid: uuid, + uuid: uuid1, foo: "bar" }, timeAdded: new Date().getTime(), msToLive: 900000 + }, + [uuid2]: { + data: { + uuid: uuid2, + foo: "baz" + }, + timeAdded: new Date().getTime(), + msToLive: 900000 } }; deepFreeze(testState); @@ -44,31 +53,31 @@ describe("objectCacheReducer", () => { it("should add the payload to the cache in response to an ADD action", () => { const state = Object.create(null); - const objectToCache = {uuid: uuid}; + const objectToCache = {uuid: uuid1}; const timeAdded = new Date().getTime(); const msToLive = 900000; const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive); const newState = objectCacheReducer(state, action); - expect(newState[uuid].data).toEqual(objectToCache); - expect(newState[uuid].timeAdded).toEqual(timeAdded); - expect(newState[uuid].msToLive).toEqual(msToLive); + expect(newState[uuid1].data).toEqual(objectToCache); + expect(newState[uuid1].timeAdded).toEqual(timeAdded); + expect(newState[uuid1].msToLive).toEqual(msToLive); }); it("should overwrite an object in the cache in response to an ADD action if it already exists", () => { - const objectToCache = {uuid: uuid, foo: "baz", somethingElse: true}; + const objectToCache = {uuid: uuid1, foo: "baz", somethingElse: true}; const timeAdded = new Date().getTime(); const msToLive = 900000; const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive); const newState = objectCacheReducer(testState, action); - expect(newState[uuid].data['foo']).toBe("baz"); - expect(newState[uuid].data['somethingElse']).toBe(true); + expect(newState[uuid1].data['foo']).toBe("baz"); + expect(newState[uuid1].data['somethingElse']).toBe(true); }); it("should perform the ADD action without affecting the previous state", () => { const state = Object.create(null); - const objectToCache = {uuid: uuid}; + const objectToCache = {uuid: uuid1}; const timeAdded = new Date().getTime(); const msToLive = 900000; const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive); @@ -78,11 +87,11 @@ describe("objectCacheReducer", () => { }); it("should remove the specified object from the cache in response to the REMOVE action", () => { - const action = new RemoveFromObjectCacheAction(uuid); + const action = new RemoveFromObjectCacheAction(uuid1); const newState = objectCacheReducer(testState, action); - expect(testState[uuid]).not.toBeUndefined(); - expect(newState[uuid]).toBeUndefined(); + expect(testState[uuid1]).not.toBeUndefined(); + expect(newState[uuid1]).toBeUndefined(); }); it("shouldn't do anything in response to the REMOVE action for an object that isn't cached", () => { @@ -93,7 +102,22 @@ describe("objectCacheReducer", () => { }); it("should perform the REMOVE action without affecting the previous state", () => { - const action = new RemoveFromObjectCacheAction(uuid); + const action = new RemoveFromObjectCacheAction(uuid1); + //testState has already been frozen above + objectCacheReducer(testState, action); + }); + + it("should set the timestamp of all objects in the cache in response to a RESET_TIMESTAMPS action", () => { + const newTimestamp = new Date().getTime(); + const action = new ResetObjectCacheTimestampsAction(newTimestamp); + const newState = objectCacheReducer(testState, action); + Object.keys(newState).forEach((key) => { + expect(newState[key].timeAdded).toEqual(newTimestamp); + }); + }); + + it("should perform the RESET_TIMESTAMPS action without affecting the previous state", () => { + const action = new ResetObjectCacheTimestampsAction(new Date().getTime()); //testState has already been frozen above objectCacheReducer(testState, action); }); diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 5e0eacdc74..23b0188216 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -1,4 +1,7 @@ -import { ObjectCacheAction, ObjectCacheActionTypes, AddToObjectCacheAction, RemoveFromObjectCacheAction } from "./object-cache.actions"; +import { + ObjectCacheAction, ObjectCacheActionTypes, AddToObjectCacheAction, + RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction +} from "./object-cache.actions"; import { hasValue } from "../../shared/empty.util"; import { CacheEntry } from "./cache-entry"; @@ -54,6 +57,10 @@ export const objectCacheReducer = (state = initialState, action: ObjectCacheActi return removeFromObjectCache(state, action) } + case ObjectCacheActionTypes.RESET_TIMESTAMPS: { + return resetObjectCacheTimestamps(state, action) + } + default: { return state; } @@ -101,3 +108,23 @@ function removeFromObjectCache(state: ObjectCacheState, action: RemoveFromObject return state; } } + +/** + * Set the timeAdded timestamp of every cached object to the specified value + * + * @param state + * the current state + * @param action + * a ResetObjectCacheTimestampsAction + * @return ObjectCacheState + * the new state, with all timeAdded timestamps set to the specified value + */ +function resetObjectCacheTimestamps(state: ObjectCacheState, action: ResetObjectCacheTimestampsAction): ObjectCacheState { + let newState = Object.create(null); + Object.keys(state).forEach(key => { + newState[key] = Object.assign({}, state[key], { + timeAdded: action.payload + }); + }); + return newState; +} diff --git a/src/app/core/cache/request-cache.actions.ts b/src/app/core/cache/request-cache.actions.ts index 85d7ca7fa3..a5d7e73ef4 100644 --- a/src/app/core/cache/request-cache.actions.ts +++ b/src/app/core/cache/request-cache.actions.ts @@ -9,7 +9,8 @@ export const RequestCacheActionTypes = { FIND_ALL: type('dspace/core/cache/request/FIND_ALL'), SUCCESS: type('dspace/core/cache/request/SUCCESS'), ERROR: type('dspace/core/cache/request/ERROR'), - REMOVE: type('dspace/core/cache/request/REMOVE') + REMOVE: type('dspace/core/cache/request/REMOVE'), + RESET_TIMESTAMPS: type('dspace/core/cache/request/RESET_TIMESTAMPS') }; export class RequestCacheFindAllAction implements Action { @@ -103,9 +104,19 @@ export class RequestCacheRemoveAction implements Action { } } +export class ResetRequestCacheTimestampsAction implements Action { + type = RequestCacheActionTypes.RESET_TIMESTAMPS; + payload: number; + + constructor(newTimestamp: number) { + this.payload = newTimestamp; + } +} + export type RequestCacheAction = RequestCacheFindAllAction | RequestCacheFindByIDAction | RequestCacheSuccessAction | RequestCacheErrorAction - | RequestCacheRemoveAction; + | RequestCacheRemoveAction + | ResetRequestCacheTimestampsAction; diff --git a/src/app/core/cache/request-cache.reducer.ts b/src/app/core/cache/request-cache.reducer.ts index 3378d55d53..340b023548 100644 --- a/src/app/core/cache/request-cache.reducer.ts +++ b/src/app/core/cache/request-cache.reducer.ts @@ -3,7 +3,7 @@ import { SortOptions } from "../shared/sort-options.model"; import { RequestCacheAction, RequestCacheActionTypes, RequestCacheFindAllAction, RequestCacheSuccessAction, RequestCacheErrorAction, RequestCacheFindByIDAction, - RequestCacheRemoveAction + RequestCacheRemoveAction, ResetRequestCacheTimestampsAction } from "./request-cache.actions"; import { OpaqueToken } from "@angular/core"; import { CacheEntry } from "./cache-entry"; @@ -54,6 +54,10 @@ export const requestCacheReducer = (state = initialState, action: RequestCacheAc return removeFromCache(state, action); } + case RequestCacheActionTypes.RESET_TIMESTAMPS: { + return resetRequestCacheTimestamps(state, action) + } + default: { return state; } @@ -121,5 +125,12 @@ function removeFromCache(state: RequestCacheState, action: RequestCacheRemoveAct } } - - +function resetRequestCacheTimestamps(state: RequestCacheState, action: ResetRequestCacheTimestampsAction): RequestCacheState { + let newState = Object.create(null); + Object.keys(state).forEach(key => { + newState[key] = Object.assign({}, state[key], { + timeAdded: action.payload + }); + }); + return newState; +} diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts index cc9835dd2d..b2d6c95ad5 100644 --- a/src/app/core/core.effects.ts +++ b/src/app/core/core.effects.ts @@ -1,8 +1,12 @@ import { EffectsModule } from "@ngrx/effects"; import { CollectionDataEffects } from "./data-services/collection-data.effects"; import { ItemDataEffects } from "./data-services/item-data.effects"; +import { ObjectCacheEffects } from "./data-services/object-cache.effects"; +import { RequestCacheEffects } from "./data-services/request-cache.effects"; export const coreEffects = [ EffectsModule.run(CollectionDataEffects), - EffectsModule.run(ItemDataEffects) + EffectsModule.run(ItemDataEffects), + EffectsModule.run(RequestCacheEffects), + EffectsModule.run(ObjectCacheEffects), ]; diff --git a/src/app/core/data-services/object-cache.effects.ts b/src/app/core/data-services/object-cache.effects.ts new file mode 100644 index 0000000000..26f13ea1b5 --- /dev/null +++ b/src/app/core/data-services/object-cache.effects.ts @@ -0,0 +1,28 @@ +import { Injectable } from "@angular/core"; +import { Actions, Effect } from "@ngrx/effects"; +import { StoreActionTypes } from "../../store.actions"; +import { ResetObjectCacheTimestampsAction } from "../cache/object-cache.actions"; +import { Store } from "@ngrx/store"; +import { ObjectCacheState } from "../cache/object-cache.reducer"; + +@Injectable() +export class ObjectCacheEffects { + + constructor( + private actions$: Actions, + private store: Store + ) { } + + /** + * When the store is rehydrated in the browser, set all cache + * timestamps to "now", because the time zone of the server can + * differ from the client. + * + * This assumes that the server cached everything a negligible + * time ago, and will likely need to be revisited later + */ + @Effect() fixTimestampsOnRehydrate = this.actions$ + .ofType(StoreActionTypes.REHYDRATE) + .map(() => new ResetObjectCacheTimestampsAction(new Date().getTime())); + +} diff --git a/src/app/core/data-services/request-cache.effects.ts b/src/app/core/data-services/request-cache.effects.ts new file mode 100644 index 0000000000..b8dde51159 --- /dev/null +++ b/src/app/core/data-services/request-cache.effects.ts @@ -0,0 +1,36 @@ +import { Injectable } from "@angular/core"; +import { Actions, Effect } from "@ngrx/effects"; +import { ResetRequestCacheTimestampsAction } from "../cache/request-cache.actions"; +import { Store } from "@ngrx/store"; +import { RequestCacheState } from "../cache/request-cache.reducer"; +import { ObjectCacheActionTypes } from "../cache/object-cache.actions"; + +@Injectable() +export class RequestCacheEffects { + + constructor( + private actions$: Actions, + private store: Store + ) { } + + /** + * When the store is rehydrated in the browser, set all cache + * timestamps to "now", because the time zone of the server can + * differ from the client. + * + * This assumes that the server cached everything a negligible + * time ago, and will likely need to be revisited later + * + * This effect should listen for StoreActionTypes.REHYDRATE, + * but can't because you can only have one effect listen to + * an action atm. Github issue: + * https://github.com/ngrx/effects/issues/87 + * + * It's listening for ObjectCacheActionTypes.RESET_TIMESTAMPS + * instead, until there's a solution. + */ + @Effect() fixTimestampsOnRehydrate = this.actions$ + .ofType(ObjectCacheActionTypes.RESET_TIMESTAMPS) + .map(() => new ResetRequestCacheTimestampsAction(new Date().getTime())); + +}