diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e65c2a2c41..bda3ccc9ea 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -10,13 +10,6 @@ import { AppComponent } from './app.component'; import { HeaderComponent } from './header/header.component'; import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component'; -import { StoreModule } from "@ngrx/store"; -import { RouterStoreModule } from "@ngrx/router-store"; -import { StoreDevtoolsModule } from "@ngrx/store-devtools"; - -import { rootReducer } from './app.reducers'; -import { effects } from './app.effects'; - @NgModule({ declarations: [ AppComponent, @@ -28,34 +21,6 @@ import { effects } from './app.effects'; HomeModule, CoreModule.forRoot(), AppRoutingModule, - /** - * StoreModule.provideStore is imported once in the root module, accepting a reducer - * function or object map of reducer functions. If passed an object of - * reducers, combineReducers will be run creating your application - * meta-reducer. This returns all providers for an @ngrx/store - * based application. - */ - StoreModule.provideStore(rootReducer), - - /** - * @ngrx/router-store keeps router state up-to-date in the store and uses - * the store as the single source of truth for the router's state. - */ - RouterStoreModule.connectRouter(), - - /** - * Store devtools instrument the store retaining past versions of state - * and recalculating new states. This enables powerful time-travel - * debugging. - * - * To use the debugger, install the Redux Devtools extension for either - * Chrome or Firefox - * - * See: https://github.com/zalmoxisus/redux-devtools-extension - */ - StoreDevtoolsModule.instrumentOnlyWithExtension(), - - effects ], providers: [ ] diff --git a/src/app/app.reducers.ts b/src/app/app.reducers.ts index db27ecf9a7..407a334144 100644 --- a/src/app/app.reducers.ts +++ b/src/app/app.reducers.ts @@ -3,6 +3,7 @@ import { routerReducer, RouterState } from "@ngrx/router-store"; import { headerReducer, HeaderState } from './header/header.reducer'; import { hostWindowReducer, HostWindowState } from "./shared/host-window.reducer"; import { CoreState, coreReducer } from "./core/core.reducers"; +import { StoreActionTypes } from "./store.actions"; export interface AppState { core: CoreState; @@ -19,5 +20,10 @@ export const reducers = { }; export function rootReducer(state: any, action: any) { + if (action.type === StoreActionTypes.REHYDRATE) { + state = action.payload; + } return combineReducers(reducers)(state, action); } + +export const NGRX_CACHE_KEY = "NGRX_STORE"; diff --git a/src/app/core/cache/cache-entry.ts b/src/app/core/cache/cache-entry.ts new file mode 100644 index 0000000000..52ec7b8af0 --- /dev/null +++ b/src/app/core/cache/cache-entry.ts @@ -0,0 +1,4 @@ +export interface CacheEntry { + timeAdded: number; + msToLive: number; +} diff --git a/src/app/core/cache/object-cache.actions.ts b/src/app/core/cache/object-cache.actions.ts index 1053ea66ed..21855e5170 100644 --- a/src/app/core/cache/object-cache.actions.ts +++ b/src/app/core/cache/object-cache.actions.ts @@ -11,11 +11,12 @@ export class AddToObjectCacheAction implements Action { type = ObjectCacheActionTypes.ADD; payload: { objectToCache: CacheableObject; + timeAdded: number; msToLive: number; }; - constructor(objectToCache: CacheableObject, msToLive: number) { - this.payload = { objectToCache, msToLive }; + constructor(objectToCache: CacheableObject, timeAdded: number, msToLive: number) { + this.payload = { objectToCache, timeAdded, msToLive }; } } diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 9e3b6f50e9..93e9f6ff05 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -1,11 +1,12 @@ import { ObjectCacheAction, ObjectCacheActionTypes, AddToObjectCacheAction, RemoveFromObjectCacheAction } from "./object-cache.actions"; import { hasValue } from "../../shared/empty.util"; +import { CacheEntry } from "./cache-entry"; export interface CacheableObject { uuid: string; } -export interface ObjectCacheEntry { +export class ObjectCacheEntry implements CacheEntry { data: CacheableObject; timeAdded: number; msToLive: number; @@ -39,7 +40,7 @@ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheActio return Object.assign({}, state, { [action.payload.objectToCache.uuid]: { data: action.payload.objectToCache, - timeAdded: new Date().getTime(), + timeAdded: action.payload.timeAdded, msToLive: action.payload.msToLive } }); diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index ca5d0ec658..c9d3bccf83 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -4,6 +4,7 @@ import { ObjectCacheState, ObjectCacheEntry, CacheableObject } from "./object-ca import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from "./object-cache.actions"; import { Observable } from "rxjs"; import { hasNoValue } from "../../shared/empty.util"; +import { GenericConstructor } from "../shared/generic-constructor"; @Injectable() export class ObjectCacheService { @@ -12,22 +13,23 @@ export class ObjectCacheService { ) {} add(objectToCache: CacheableObject, msToLive: number): void { - this.store.dispatch(new AddToObjectCacheAction(objectToCache, msToLive)); + this.store.dispatch(new AddToObjectCacheAction(objectToCache, new Date().getTime(), msToLive)); } remove(uuid: string): void { this.store.dispatch(new RemoveFromObjectCacheAction(uuid)); } - get(uuid: string): Observable { + get(uuid: string, ctor: GenericConstructor): Observable { return this.store.select('core', 'cache', 'object', uuid) .filter(entry => this.isValid(entry)) - .map((entry: ObjectCacheEntry) => entry.data); + .distinctUntilChanged() + .map((entry: ObjectCacheEntry) => Object.assign(new ctor(), entry.data)); } - getList(uuids: Array): Observable> { + getList(uuids: Array, ctor: GenericConstructor): Observable> { return Observable.combineLatest( - uuids.map((id: string) => this.get(id)) + uuids.map((id: string) => this.get(id, ctor)) ); } diff --git a/src/app/core/cache/request-cache.actions.ts b/src/app/core/cache/request-cache.actions.ts index 63874f9188..85d7ca7fa3 100644 --- a/src/app/core/cache/request-cache.actions.ts +++ b/src/app/core/cache/request-cache.actions.ts @@ -5,14 +5,15 @@ import { PaginationOptions } from "../shared/pagination-options.model"; import { SortOptions } from "../shared/sort-options.model"; export const RequestCacheActionTypes = { - FIND_BY_ID_REQUEST: type('dspace/core/cache/request/FIND_BY_ID_REQUEST'), - FIND_ALL_REQUEST: type('dspace/core/cache/request/FIND_ALL_REQUEST'), + FIND_BY_ID: type('dspace/core/cache/request/FIND_BY_ID'), + FIND_ALL: type('dspace/core/cache/request/FIND_ALL'), SUCCESS: type('dspace/core/cache/request/SUCCESS'), - ERROR: type('dspace/core/cache/request/ERROR') + ERROR: type('dspace/core/cache/request/ERROR'), + REMOVE: type('dspace/core/cache/request/REMOVE') }; -export class FindAllRequestCacheAction implements Action { - type = RequestCacheActionTypes.FIND_ALL_REQUEST; +export class RequestCacheFindAllAction implements Action { + type = RequestCacheActionTypes.FIND_ALL; payload: { key: string, service: OpaqueToken, @@ -38,8 +39,8 @@ export class FindAllRequestCacheAction implements Action { } } -export class FindByIDRequestCacheAction implements Action { - type = RequestCacheActionTypes.FIND_BY_ID_REQUEST; +export class RequestCacheFindByIDAction implements Action { + type = RequestCacheActionTypes.FIND_BY_ID; payload: { key: string, service: OpaqueToken, @@ -64,13 +65,15 @@ export class RequestCacheSuccessAction implements Action { payload: { key: string, resourceUUIDs: Array, + timeAdded: number, msToLive: number }; - constructor(key: string, resourceUUIDs: Array, msToLive: number) { + constructor(key: string, resourceUUIDs: Array, timeAdded, msToLive: number) { this.payload = { key, resourceUUIDs, + timeAdded, msToLive }; } @@ -91,8 +94,18 @@ export class RequestCacheErrorAction implements Action { } } +export class RequestCacheRemoveAction implements Action { + type = RequestCacheActionTypes.REMOVE; + payload: string; + + constructor(key: string) { + this.payload = key; + } +} + export type RequestCacheAction - = FindAllRequestCacheAction - | FindByIDRequestCacheAction + = RequestCacheFindAllAction + | RequestCacheFindByIDAction | RequestCacheSuccessAction - | RequestCacheErrorAction; + | RequestCacheErrorAction + | RequestCacheRemoveAction; diff --git a/src/app/core/cache/request-cache.reducer.ts b/src/app/core/cache/request-cache.reducer.ts index 051b029701..b867dcbdf8 100644 --- a/src/app/core/cache/request-cache.reducer.ts +++ b/src/app/core/cache/request-cache.reducer.ts @@ -1,13 +1,17 @@ import { PaginationOptions } from "../shared/pagination-options.model"; import { SortOptions } from "../shared/sort-options.model"; import { - RequestCacheAction, RequestCacheActionTypes, FindAllRequestCacheAction, - RequestCacheSuccessAction, RequestCacheErrorAction, FindByIDRequestCacheAction + RequestCacheAction, RequestCacheActionTypes, RequestCacheFindAllAction, + RequestCacheSuccessAction, RequestCacheErrorAction, RequestCacheFindByIDAction, + RequestCacheRemoveAction } from "./request-cache.actions"; import { OpaqueToken } from "@angular/core"; +import { CacheEntry } from "./cache-entry"; +import { hasValue } from "../../shared/empty.util"; -export interface CachedRequest { - service: OpaqueToken +export class RequestCacheEntry implements CacheEntry { + service: OpaqueToken; + key: string; scopeID: string; resourceID: string; resourceUUIDs: Array; @@ -21,7 +25,7 @@ export interface CachedRequest { } export interface RequestCacheState { - [key: string]: CachedRequest + [key: string]: RequestCacheEntry } // Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) @@ -30,12 +34,12 @@ const initialState = Object.create(null); export const requestCacheReducer = (state = initialState, action: RequestCacheAction): RequestCacheState => { switch (action.type) { - case RequestCacheActionTypes.FIND_ALL_REQUEST: { - return findAllRequest(state, action); + case RequestCacheActionTypes.FIND_ALL: { + return findAllRequest(state, action); } - case RequestCacheActionTypes.FIND_BY_ID_REQUEST: { - return findByIDRequest(state, action); + case RequestCacheActionTypes.FIND_BY_ID: { + return findByIDRequest(state, action); } case RequestCacheActionTypes.SUCCESS: { @@ -46,15 +50,21 @@ export const requestCacheReducer = (state = initialState, action: RequestCacheAc return error(state, action); } + case RequestCacheActionTypes.REMOVE: { + return removeFromCache(state, action); + } + default: { return state; } } }; -function findAllRequest(state: RequestCacheState, action: FindAllRequestCacheAction): RequestCacheState { +function findAllRequest(state: RequestCacheState, action: RequestCacheFindAllAction): RequestCacheState { + console.log('break here', state); return Object.assign({}, state, { [action.payload.key]: { + key: action.payload.key, service: action.payload.service, scopeID: action.payload.scopeID, resourceUUIDs: [], @@ -66,9 +76,10 @@ function findAllRequest(state: RequestCacheState, action: FindAllRequestCacheAct }); } -function findByIDRequest(state: RequestCacheState, action: FindByIDRequestCacheAction): RequestCacheState { +function findByIDRequest(state: RequestCacheState, action: RequestCacheFindByIDAction): RequestCacheState { return Object.assign({}, state, { [action.payload.key]: { + key: action.payload.key, service: action.payload.service, resourceID: action.payload.resourceID, resourceUUIDs: [], @@ -84,7 +95,7 @@ function success(state: RequestCacheState, action: RequestCacheSuccessAction): R isLoading: false, resourceUUIDs: action.payload.resourceUUIDs, errorMessage: undefined, - timeAdded: new Date().getTime(), + timeAdded: action.payload.timeAdded, msToLive: action.payload.msToLive }) }); @@ -99,4 +110,17 @@ function error(state: RequestCacheState, action: RequestCacheErrorAction): Reque }); } +function removeFromCache(state: RequestCacheState, action: RequestCacheRemoveAction): RequestCacheState { + if (hasValue(state[action.payload])) { + let newCache = Object.assign({}, state); + delete newCache[action.payload]; + + return newCache; + } + else { + return state; + } +} + + diff --git a/src/app/core/cache/request-cache.service.ts b/src/app/core/cache/request-cache.service.ts new file mode 100644 index 0000000000..418a1ad204 --- /dev/null +++ b/src/app/core/cache/request-cache.service.ts @@ -0,0 +1,73 @@ +import { Injectable, OpaqueToken } from "@angular/core"; +import { Store } from "@ngrx/store"; +import { RequestCacheState, RequestCacheEntry } from "./request-cache.reducer"; +import { Observable } from "rxjs"; +import { hasNoValue } from "../../shared/empty.util"; +import { + RequestCacheRemoveAction, RequestCacheFindAllAction, + RequestCacheFindByIDAction +} from "./request-cache.actions"; +import { SortOptions } from "../shared/sort-options.model"; +import { PaginationOptions } from "../shared/pagination-options.model"; + +@Injectable() +export class RequestCacheService { + constructor( + private store: Store + ) {} + + findAll( + key: string, + service: OpaqueToken, + scopeID?: string, + paginationOptions?: PaginationOptions, + sortOptions?: SortOptions + ): Observable { + if (!this.has(key)) { + this.store.dispatch(new RequestCacheFindAllAction(key, service, scopeID, paginationOptions, sortOptions)); + } + return this.get(key); + } + + findById( + key: string, + service: OpaqueToken, + resourceID: string + ): Observable { + if (!this.has(key)) { + this.store.dispatch(new RequestCacheFindByIDAction(key, service, resourceID)); + } + return this.get(key); + } + + get(key: string): Observable { + return this.store.select('core', 'cache', 'request', key) + .filter(entry => this.isValid(entry)) + .distinctUntilChanged() + } + + has(key: string): boolean { + let result: boolean; + + this.store.select('core', 'cache', 'request', key) + .take(1) + .subscribe(entry => result = this.isValid(entry)); + + return result; + } + + private isValid(entry: RequestCacheEntry): boolean { + if (hasNoValue(entry)) { + return false; + } + else { + const timeOutdated = entry.timeAdded + entry.msToLive; + const isOutDated = new Date().getTime() > timeOutdated; + if (isOutDated) { + this.store.dispatch(new RequestCacheRemoveAction(entry.key)); + } + return !isOutDated; + } + } + +} diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index a2fd79ffbc..5aa2b149a5 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -5,6 +5,7 @@ import { isNotEmpty } from "../shared/empty.util"; import { FooterComponent } from "./footer/footer.component"; import { DSpaceRESTv2Service } from "./dspace-rest-v2/dspace-rest-v2.service"; import { ObjectCacheService } from "./cache/object-cache.service"; +import { RequestCacheService } from "./cache/request-cache.service"; import { CollectionDataService } from "./data-services/collection-data.service"; import { ItemDataService } from "./data-services/item-data.service"; @@ -25,7 +26,8 @@ const PROVIDERS = [ CollectionDataService, ItemDataService, DSpaceRESTv2Service, - ObjectCacheService + ObjectCacheService, + RequestCacheService ]; @NgModule({ diff --git a/src/app/core/data-services/collection-data.effects.ts b/src/app/core/data-services/collection-data.effects.ts index 03bec274bc..676d71659c 100644 --- a/src/app/core/data-services/collection-data.effects.ts +++ b/src/app/core/data-services/collection-data.effects.ts @@ -6,7 +6,7 @@ import { DSpaceRESTv2Serializer } from "../dspace-rest-v2/dspace-rest-v2.seriali import { ObjectCacheService } from "../cache/object-cache.service"; import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service"; import { Actions, Effect } from "@ngrx/effects"; -import { FindAllRequestCacheAction, FindByIDRequestCacheAction } from "../cache/request-cache.actions"; +import { RequestCacheFindAllAction, RequestCacheFindByIDAction } from "../cache/request-cache.actions"; import { CollectionDataService } from "./collection-data.service"; @Injectable() @@ -20,11 +20,11 @@ export class CollectionDataEffects extends DataEffects { super(actions$, restApi, cache, dataService); } - protected getFindAllEndpoint(action: FindAllRequestCacheAction): string { + protected getFindAllEndpoint(action: RequestCacheFindAllAction): string { return '/collections'; } - protected getFindByIdEndpoint(action: FindByIDRequestCacheAction): string { + protected getFindByIdEndpoint(action: RequestCacheFindByIDAction): string { return `/collections/${action.payload.resourceID}`; } diff --git a/src/app/core/data-services/collection-data.service.ts b/src/app/core/data-services/collection-data.service.ts index fdfaaa97ac..cc850900db 100644 --- a/src/app/core/data-services/collection-data.service.ts +++ b/src/app/core/data-services/collection-data.service.ts @@ -1,19 +1,18 @@ import { Injectable, OpaqueToken } from "@angular/core"; -import { Store } from "@ngrx/store"; import { DataService } from "./data.service"; import { Collection } from "../shared/collection.model"; import { ObjectCacheService } from "../cache/object-cache.service"; -import { RequestCacheState } from "../cache/request-cache.reducer"; +import { RequestCacheService } from "../cache/request-cache.service"; @Injectable() export class CollectionDataService extends DataService { - name = new OpaqueToken('CollectionDataService'); + serviceName = new OpaqueToken('CollectionDataService'); constructor( - store: Store, - cache: ObjectCacheService + protected objectCache: ObjectCacheService, + protected requestCache: RequestCacheService, ) { - super(store, cache); + super(Collection); } } diff --git a/src/app/core/data-services/data.effects.ts b/src/app/core/data-services/data.effects.ts index ee7318da12..c39a718732 100644 --- a/src/app/core/data-services/data.effects.ts +++ b/src/app/core/data-services/data.effects.ts @@ -1,4 +1,4 @@ -import { Actions, Effect } from "@ngrx/effects"; +import { Actions } from "@ngrx/effects"; import { Observable } from "rxjs"; import { DSpaceRESTV2Response } from "../dspace-rest-v2/dspace-rest-v2-response.model"; import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service"; @@ -7,14 +7,14 @@ import { GlobalConfig } from "../../../config"; import { CacheableObject } from "../cache/object-cache.reducer"; import { Serializer } from "../serializer"; import { - RequestCacheActionTypes, FindAllRequestCacheAction, RequestCacheSuccessAction, - RequestCacheErrorAction, FindByIDRequestCacheAction + RequestCacheActionTypes, RequestCacheFindAllAction, RequestCacheSuccessAction, + RequestCacheErrorAction, RequestCacheFindByIDAction } from "../cache/request-cache.actions"; import { DataService } from "./data.service"; export abstract class DataEffects { - protected abstract getFindAllEndpoint(action: FindAllRequestCacheAction): string; - protected abstract getFindByIdEndpoint(action: FindByIDRequestCacheAction): string; + protected abstract getFindAllEndpoint(action: RequestCacheFindAllAction): string; + protected abstract getFindByIdEndpoint(action: RequestCacheFindByIDAction): string; protected abstract getSerializer(): Serializer; constructor( @@ -24,13 +24,11 @@ export abstract class DataEffects { private dataService: DataService ) {} - // TODO, results of a findall aren't retrieved from cache for now, - // because currently the cache is more of an object store. We need to move - // more towards memoization for things like this. + // TODO, results of a findall aren't retrieved from cache yet protected findAll = this.actions$ - .ofType(RequestCacheActionTypes.FIND_ALL_REQUEST) - .filter((action: FindAllRequestCacheAction) => action.payload.service === this.dataService.name) - .flatMap((action: FindAllRequestCacheAction) => { + .ofType(RequestCacheActionTypes.FIND_ALL) + .filter((action: RequestCacheFindAllAction) => action.payload.service === this.dataService.serviceName) + .flatMap((action: RequestCacheFindAllAction) => { //TODO scope, pagination, sorting -> when we know how that works in rest return this.restApi.get(this.getFindAllEndpoint(action)) .map((data: DSpaceRESTV2Response) => this.getSerializer().deserializeArray(data)) @@ -40,20 +38,20 @@ export abstract class DataEffects { }); }) .map((ts: Array) => ts.map(t => t.uuid)) - .map((ids: Array) => new RequestCacheSuccessAction(action.payload.key, ids, GlobalConfig.cache.msToLive)) + .map((ids: Array) => new RequestCacheSuccessAction(action.payload.key, ids, new Date().getTime(), GlobalConfig.cache.msToLive)) .catch((errorMsg: string) => Observable.of(new RequestCacheErrorAction(action.payload.key, errorMsg))); }); protected findById = this.actions$ - .ofType(RequestCacheActionTypes.FIND_BY_ID_REQUEST) - .filter((action: FindAllRequestCacheAction) => action.payload.service === this.dataService.name) - .flatMap((action: FindByIDRequestCacheAction) => { + .ofType(RequestCacheActionTypes.FIND_BY_ID) + .filter((action: RequestCacheFindAllAction) => action.payload.service === this.dataService.serviceName) + .flatMap((action: RequestCacheFindByIDAction) => { return this.restApi.get(this.getFindByIdEndpoint(action)) .map((data: DSpaceRESTV2Response) => this.getSerializer().deserialize(data)) .do((t: T) => { this.objectCache.add(t, GlobalConfig.cache.msToLive); }) - .map((t: T) => new RequestCacheSuccessAction(action.payload.key, [t.uuid], GlobalConfig.cache.msToLive)) + .map((t: T) => new RequestCacheSuccessAction(action.payload.key, [t.uuid], new Date().getTime(), GlobalConfig.cache.msToLive)) .catch((errorMsg: string) => Observable.of(new RequestCacheErrorAction(action.payload.key, errorMsg))); }); diff --git a/src/app/core/data-services/data.service.ts b/src/app/core/data-services/data.service.ts index 120245c769..07414a0ce7 100644 --- a/src/app/core/data-services/data.service.ts +++ b/src/app/core/data-services/data.service.ts @@ -1,39 +1,39 @@ import { OpaqueToken } from "@angular/core"; import { Observable } from "rxjs"; -import { Store } from "@ngrx/store"; import { ObjectCacheService } from "../cache/object-cache.service"; +import { RequestCacheService } from "../cache/request-cache.service"; import { CacheableObject } from "../cache/object-cache.reducer"; -import { RequestCacheState } from "../cache/request-cache.reducer"; -import { FindAllRequestCacheAction, FindByIDRequestCacheAction } from "../cache/request-cache.actions"; import { ParamHash } from "../shared/param-hash"; import { isNotEmpty } from "../../shared/empty.util"; +import { GenericConstructor } from "../shared/generic-constructor"; export abstract class DataService { - abstract name: OpaqueToken; + abstract serviceName: OpaqueToken; + protected abstract objectCache: ObjectCacheService; + protected abstract requestCache: RequestCacheService; - constructor( - private store: Store, - private objectCache: ObjectCacheService - ) { } + constructor(private modelType: GenericConstructor) { + + } findAll(scopeID?: string): Observable> { - const key = new ParamHash(this.name, 'findAll', scopeID).toString(); - this.store.dispatch(new FindAllRequestCacheAction(key, this.name, scopeID)); - //get an observable of the IDs from the store - return this.store.select>('core', 'cache', 'request', key, 'resourceUUIDs') + const key = new ParamHash(this.serviceName, 'findAll', scopeID).toString(); + return this.requestCache.findAll(key, this.serviceName, scopeID) + //get an observable of the IDs from the RequestCache + .map(entry => entry.resourceUUIDs) .flatMap((resourceUUIDs: Array) => { - // use those IDs to fetch the actual objects from the cache - return this.objectCache.getList(resourceUUIDs); + // use those IDs to fetch the actual objects from the ObjectCache + return this.objectCache.getList(resourceUUIDs, this.modelType); }); } findById(id: string): Observable { - const key = new ParamHash(this.name, 'findById', id).toString(); - this.store.dispatch(new FindByIDRequestCacheAction(key, this.name, id)); - return this.store.select>('core', 'cache', 'request', key, 'resourceUUIDs') + const key = new ParamHash(this.serviceName, 'findById', id).toString(); + return this.requestCache.findById(key, this.serviceName, id) + .map(entry => entry.resourceUUIDs) .flatMap((resourceUUIDs: Array) => { if(isNotEmpty(resourceUUIDs)) { - return this.objectCache.get(resourceUUIDs[0]); + return this.objectCache.get(resourceUUIDs[0], this.modelType); } else { return Observable.of(undefined); diff --git a/src/app/core/data-services/item-data.effects.ts b/src/app/core/data-services/item-data.effects.ts index c1b8fb5ef2..d6bce6e8da 100644 --- a/src/app/core/data-services/item-data.effects.ts +++ b/src/app/core/data-services/item-data.effects.ts @@ -6,7 +6,7 @@ import { DSpaceRESTv2Serializer } from "../dspace-rest-v2/dspace-rest-v2.seriali import { ObjectCacheService } from "../cache/object-cache.service"; import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service"; import { Actions, Effect } from "@ngrx/effects"; -import { FindAllRequestCacheAction, FindByIDRequestCacheAction } from "../cache/request-cache.actions"; +import { RequestCacheFindAllAction, RequestCacheFindByIDAction } from "../cache/request-cache.actions"; import { ItemDataService } from "./item-data.service"; @Injectable() @@ -20,11 +20,11 @@ export class ItemDataEffects extends DataEffects { super(actions$, restApi, cache, dataService); } - protected getFindAllEndpoint(action: FindAllRequestCacheAction): string { + protected getFindAllEndpoint(action: RequestCacheFindAllAction): string { return '/items'; } - protected getFindByIdEndpoint(action: FindByIDRequestCacheAction): string { + protected getFindByIdEndpoint(action: RequestCacheFindByIDAction): string { return `/items/${action.payload.resourceID}`; } diff --git a/src/app/core/data-services/item-data.service.ts b/src/app/core/data-services/item-data.service.ts index 86b6bf9678..f3c8fd83af 100644 --- a/src/app/core/data-services/item-data.service.ts +++ b/src/app/core/data-services/item-data.service.ts @@ -1,19 +1,18 @@ import { Injectable, OpaqueToken } from "@angular/core"; -import { Store } from "@ngrx/store"; import { DataService } from "./data.service"; import { Item } from "../shared/item.model"; import { ObjectCacheService } from "../cache/object-cache.service"; -import { RequestCacheState } from "../cache/request-cache.reducer"; +import { RequestCacheService } from "../cache/request-cache.service"; @Injectable() export class ItemDataService extends DataService { - name = new OpaqueToken('ItemDataService'); + serviceName = new OpaqueToken('ItemDataService'); constructor( - store: Store, - cache: ObjectCacheService + protected objectCache: ObjectCacheService, + protected requestCache: RequestCacheService, ) { - super(store, cache); + super(Item); } } diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts index 3f457daa94..b5fa5983d8 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts @@ -2,13 +2,7 @@ import { Serialize, Deserialize } from "cerialize"; import { Serializer } from "../serializer"; import { DSpaceRESTV2Response } from "./dspace-rest-v2-response.model"; import { DSpaceRESTv2Validator } from "./dspace-rest-v2.validator"; - -/** - * ensures we can use 'typeof T' as a type - * more details: - * https://github.com/Microsoft/TypeScript/issues/204#issuecomment-257722306 - */ -type Constructor = { new (...args: any[]): T } | ((...args: any[]) => T) | Function; +import { GenericConstructor } from "../shared/generic-constructor"; /** * This Serializer turns responses from v2 of DSpace's REST API @@ -22,7 +16,7 @@ export class DSpaceRESTv2Serializer implements Serializer { * @param modelType a class or interface to indicate * the kind of model this serializer should work with */ - constructor(private modelType: Constructor) { + constructor(private modelType: GenericConstructor) { } /** diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index 3e480bbb09..b990c8617e 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -5,18 +5,19 @@ import { Item } from "./item.model"; @inheritSerialization(DSpaceObject) export class Bundle extends DSpaceObject { - /** - * The primary bitstream of this Bundle - */ - primaryBitstream: Bitstream; + /** + * The primary bitstream of this Bundle + */ + primaryBitstream: Bitstream; - /** - * An array of Items that are direct parents of this Bundle - */ - parents: Array; + /** + * An array of Items that are direct parents of this Bundle + */ + parents: Array; + + /** + * The Item that owns this Bundle + */ + owner: Item; - /** - * The Item that owns this Bundle - */ - owner: Item; } diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 4318e6b80e..395886655f 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -8,44 +8,44 @@ import { CacheableObject } from "../cache/object-cache.reducer"; */ export abstract class DSpaceObject implements CacheableObject { - /** - * The human-readable identifier of this DSpaceObject - */ - @autoserialize - id: string; + /** + * The human-readable identifier of this DSpaceObject + */ + @autoserialize + id: string; - /** - * The universally unique identifier of this DSpaceObject - */ - @autoserialize - uuid: string; + /** + * The universally unique identifier of this DSpaceObject + */ + @autoserialize + uuid: string; - /** - * A string representing the kind of DSpaceObject, e.g. community, item, … - */ - type: string; + /** + * A string representing the kind of DSpaceObject, e.g. community, item, … + */ + type: string; - /** - * The name for this DSpaceObject - */ - @autoserialize - name: string; + /** + * The name for this DSpaceObject + */ + @autoserialize + name: string; - /** - * An array containing all metadata of this DSpaceObject - */ - @autoserializeAs(Metadatum) - metadata: Array; + /** + * An array containing all metadata of this DSpaceObject + */ + @autoserializeAs(Metadatum) + metadata: Array; - /** - * An array of DSpaceObjects that are direct parents of this DSpaceObject - */ - parents: Array; + /** + * An array of DSpaceObjects that are direct parents of this DSpaceObject + */ + parents: Array; - /** - * The DSpaceObject that owns this DSpaceObject - */ - owner: DSpaceObject; + /** + * The DSpaceObject that owns this DSpaceObject + */ + owner: DSpaceObject; /** * Find a metadata field by key and language @@ -58,17 +58,17 @@ export abstract class DSpaceObject implements CacheableObject { * @param language * @return string */ - findMetadata(key: string, language?: string): string { - const metadatum = this.metadata - .find((metadatum: Metadatum) => { - return metadatum.key === key && - (isEmpty(language) || metadatum.language === language) - }); - if (isNotEmpty(metadatum)) { - return metadatum.value; - } - else { - return undefined; - } + findMetadata(key: string, language?: string): string { + const metadatum = this.metadata + .find((metadatum: Metadatum) => { + return metadatum.key === key && + (isEmpty(language) || metadatum.language === language) + }); + if (isNotEmpty(metadatum)) { + return metadatum.value; } + else { + return undefined; + } + } } diff --git a/src/app/core/shared/generic-constructor.ts b/src/app/core/shared/generic-constructor.ts new file mode 100644 index 0000000000..7ee9254fd9 --- /dev/null +++ b/src/app/core/shared/generic-constructor.ts @@ -0,0 +1,7 @@ +/** + * ensures we can use 'typeof T' as a type + * more details: + * https://github.com/Microsoft/TypeScript/issues/204#issuecomment-257722306 + */ +export type GenericConstructor = { new (...args: any[]): T }; + diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index c70d88874d..478d94f814 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -5,34 +5,35 @@ import { Collection } from "./collection.model"; @inheritSerialization(DSpaceObject) export class Item extends DSpaceObject { - /** - * A string representing the unique handle of this Item - */ - @autoserialize - handle: string; + /** + * A string representing the unique handle of this Item + */ + @autoserialize + handle: string; - /** - * The Date of the last modification of this Item - */ - lastModified: Date; + /** + * The Date of the last modification of this Item + */ + lastModified: Date; - /** - * A boolean representing if this Item is currently archived or not - */ - isArchived: boolean; + /** + * A boolean representing if this Item is currently archived or not + */ + isArchived: boolean; - /** - * A boolean representing if this Item is currently withdrawn or not - */ - isWithdrawn: boolean; + /** + * A boolean representing if this Item is currently withdrawn or not + */ + isWithdrawn: boolean; - /** - * An array of Collections that are direct parents of this Item - */ - parents: Array; + /** + * An array of Collections that are direct parents of this Item + */ + parents: Array; + + /** + * The Collection that owns this Item + */ + owner: Collection; - /** - * The Collection that owns this Item - */ - owner: Collection; } diff --git a/src/app/shared/demo-cache.service.ts b/src/app/shared/demo-cache.service.ts deleted file mode 100644 index 3bdc5ecb59..0000000000 --- a/src/app/shared/demo-cache.service.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { Inject, Injectable, isDevMode } from '@angular/core'; - -@Injectable() -export class DemoCacheService { - static KEY = 'DemoCacheService'; - - constructor( @Inject('LRU') public _cache: Map) { - - } - - /** - * check if there is a value in our store - */ - has(key: string | number): boolean { - let _key = this.normalizeKey(key); - return this._cache.has(_key); - } - - /** - * store our state - */ - set(key: string | number, value: any): void { - let _key = this.normalizeKey(key); - this._cache.set(_key, value); - } - - /** - * get our cached value - */ - get(key: string | number): any { - let _key = this.normalizeKey(key); - return this._cache.get(_key); - } - - /** - * release memory refs - */ - clear(): void { - this._cache.clear(); - } - - /** - * convert to json for the client - */ - dehydrate(): any { - let json = {}; - this._cache.forEach((value: any, key: string) => json[key] = value); - return json; - } - - /** - * convert server json into out initial state - */ - rehydrate(json: any): void { - Object.keys(json).forEach((key: string) => { - let _key = this.normalizeKey(key); - let value = json[_key]; - this._cache.set(_key, value); - }); - } - - /** - * allow JSON.stringify to work - */ - toJSON(): any { - return this.dehydrate(); - } - - /** - * convert numbers into strings - */ - normalizeKey(key: string | number): string { - if (isDevMode() && this._isInvalidValue(key)) { - throw new Error('Please provide a valid key to save in the DemoCacheService'); - } - - return key + ''; - } - - _isInvalidValue(key): boolean { - return key === null || - key === undefined || - key === 0 || - key === '' || - typeof key === 'boolean' || - Number.isNaN(key); - } -} diff --git a/src/app/shared/model/model.service.ts b/src/app/shared/model/model.service.ts deleted file mode 100644 index 734f931894..0000000000 --- a/src/app/shared/model/model.service.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; -import 'rxjs/add/observable/of'; -import 'rxjs/add/operator/do'; -import 'rxjs/add/operator/share'; - -import { DemoCacheService } from '../demo-cache.service'; -import { ApiService } from '../api.service'; - -export function hashCodeString(str: string): string { - let hash = 0; - if (str.length === 0) { - return hash + ''; - } - for (let i = 0; i < str.length; i++) { - let char = str.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; // Convert to 32bit integer - } - return hash + ''; -} - -// domain/feature service -@Injectable() -export class ModelService { - // This is only one example of one Model depending on your domain - constructor(public _api: ApiService, public _cache: DemoCacheService) { - - } - - /** - * whatever domain/feature method name - */ - get(url) { - // you want to return the cache if there is a response in it. - // This would cache the first response so if your API isn't idempotent - // you probably want to remove the item from the cache after you use it. LRU of 10 - // you can use also hashCodeString here - let key = url; - - if (this._cache.has(key)) { - return Observable.of(this._cache.get(key)); - } - // you probably shouldn't .share() and you should write the correct logic - return this._api.get(url) - .do(json => { - this._cache.set(key, json); - }) - .share(); - } - // don't cache here since we're creating - create() { - // TODO - } -} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 91b9d1b87d..1b7b0ad20e 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -7,7 +7,6 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from 'ng2-translate/ng2-translate'; import { ApiService } from './api.service'; -import { ModelService } from './model/model.service'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -28,7 +27,6 @@ const COMPONENTS = [ ]; const PROVIDERS = [ - ModelService, ApiService ]; diff --git a/src/app/store.actions.ts b/src/app/store.actions.ts new file mode 100644 index 0000000000..3bfb20e31a --- /dev/null +++ b/src/app/store.actions.ts @@ -0,0 +1,16 @@ +import { type } from "./shared/ngrx/type"; +import { Action } from "@ngrx/store"; +import { AppState } from "./app.reducers"; + +export const StoreActionTypes = { + REHYDRATE: type('dspace/ngrx/rehydrate') +}; + +export class RehydrateStoreAction implements Action { + type = StoreActionTypes.REHYDRATE; + + constructor(public payload: AppState) {} +} + +export type StoreAction + = RehydrateStoreAction; diff --git a/src/browser.module.ts b/src/browser.module.ts index 5ef364fea6..3845f77907 100755 --- a/src/browser.module.ts +++ b/src/browser.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; import { Http } from '@angular/http'; import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; -import { UniversalModule, isBrowser, isNode, AUTO_PREBOOT } from 'angular2-universal/browser'; // for AoT we need to manually split universal packages +import { UniversalModule, isBrowser, isNode } from 'angular2-universal/browser'; // for AoT we need to manually split universal packages import { IdlePreload, IdlePreloadModule } from '@angularclass/idle-preload'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; @@ -10,12 +10,18 @@ import { TranslateLoader, TranslateModule, TranslateStaticLoader } from 'ng2-tra import { AppModule, AppComponent } from './app/app.module'; import { SharedModule } from './app/shared/shared.module'; -import { DemoCacheService } from './app/shared/demo-cache.service'; import { CoreModule } from "./app/core/core.module"; +import { StoreModule, Store } from "@ngrx/store"; +import { RouterStoreModule } from "@ngrx/router-store"; +import { StoreDevtoolsModule } from "@ngrx/store-devtools"; +import { rootReducer, NGRX_CACHE_KEY, AppState } from './app/app.reducers'; +import { effects } from './app/app.effects'; + // Will be merged into @angular/platform-browser in a later release // see https://github.com/angular/angular/pull/12322 import { Meta } from './angular2-meta'; +import { RehydrateStoreAction } from "./app/store.actions"; // import * as LRU from 'modern-lru'; @@ -38,7 +44,6 @@ export function getResponse() { } -// TODO(gdi2290): refactor into Universal export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE'; @NgModule({ @@ -60,6 +65,10 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE'; CoreModule.forRoot(), SharedModule, AppModule, + StoreModule.provideStore(rootReducer), + RouterStoreModule.connectRouter(), + StoreDevtoolsModule.instrumentOnlyWithExtension(), + effects ], providers: [ { provide: 'isBrowser', useValue: isBrowser }, @@ -70,23 +79,21 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE'; { provide: 'LRU', useFactory: getLRU, deps: [] }, - DemoCacheService, - Meta, // { provide: AUTO_PREBOOT, useValue: false } // turn off auto preboot complete ] }) export class MainModule { - constructor(public cache: DemoCacheService) { + constructor(public store: Store) { // TODO(gdi2290): refactor into a lifecycle hook this.doRehydrate(); } doRehydrate() { let defaultValue = {}; - let serverCache = this._getCacheValue(DemoCacheService.KEY, defaultValue); - this.cache.rehydrate(serverCache); + let serverCache = this._getCacheValue(NGRX_CACHE_KEY, defaultValue); + this.store.dispatch(new RehydrateStoreAction(serverCache)); } _getCacheValue(key: string, defaultValue: any): any { @@ -95,7 +102,7 @@ export class MainModule { if (win[UNIVERSAL_KEY] && win[UNIVERSAL_KEY][key]) { let serverCache = defaultValue; try { - serverCache = JSON.parse(win[UNIVERSAL_KEY][key]); + serverCache = win[UNIVERSAL_KEY][key]; if (typeof serverCache !== typeof defaultValue) { console.log('Angular Universal: The type of data from the server is different from the default value type'); serverCache = defaultValue; diff --git a/src/node.module.ts b/src/node.module.ts index 6a554e0e8c..8420a76a96 100755 --- a/src/node.module.ts +++ b/src/node.module.ts @@ -9,9 +9,14 @@ import { TranslateLoader, TranslateModule, TranslateStaticLoader } from 'ng2-tra import { AppModule, AppComponent } from './app/app.module'; import { SharedModule } from './app/shared/shared.module'; -import { DemoCacheService } from './app/shared/demo-cache.service'; import { CoreModule } from "./app/core/core.module"; +import { StoreModule, Store } from "@ngrx/store"; +import { RouterStoreModule } from "@ngrx/router-store"; +import { StoreDevtoolsModule } from "@ngrx/store-devtools"; +import { rootReducer, AppState, NGRX_CACHE_KEY } from './app/app.reducers'; +import { effects } from './app/app.effects'; + // Will be merged into @angular/platform-browser in a later release // see https://github.com/angular/angular/pull/12322 import { Meta } from './angular2-meta'; @@ -30,7 +35,6 @@ export function getResponse() { return Zone.current.get('res') || {}; } -// TODO(gdi2290): refactor into Universal export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE'; @NgModule({ @@ -51,6 +55,10 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE'; CoreModule.forRoot(), SharedModule, AppModule, + StoreModule.provideStore(rootReducer), + RouterStoreModule.connectRouter(), + StoreDevtoolsModule.instrumentOnlyWithExtension(), + effects ], providers: [ { provide: 'isBrowser', useValue: isBrowser }, @@ -61,13 +69,11 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE'; { provide: 'LRU', useFactory: getLRU, deps: [] }, - DemoCacheService, - Meta, ] }) export class MainModule { - constructor(public cache: DemoCacheService) { + constructor(public store: Store) { } @@ -76,14 +82,17 @@ export class MainModule { * in Universal for now until it's fixed */ universalDoDehydrate = (universalCache) => { - universalCache[DemoCacheService.KEY] = JSON.stringify(this.cache.dehydrate()); - } + this.store.take(1).subscribe(state => { + universalCache[NGRX_CACHE_KEY] = state; + }); + }; /** * Clear the cache after it's rendered */ universalAfterDehydrate = () => { // comment out if LRU provided at platform level to be shared between each user - this.cache.clear(); + // this.cache.clear(); + //TODO is this necessary in dspace's case? } }