Moved all objects to a single data store: the cache.

This commit is contained in:
Art Lowel
2017-02-15 15:57:38 +01:00
parent c8fb98760d
commit 2e5441d6f7
18 changed files with 240 additions and 55 deletions

View File

@@ -10,5 +10,9 @@ module.exports = {
"ui": { "ui": {
"nameSpace": "/", "nameSpace": "/",
"baseURL": "http://localhost:3000" "baseURL": "http://localhost:3000"
},
"cache": {
// how long should objects be cached for by default
"msToLive": 15 * 60 * 1000 //15 minutes
} }
} };

View File

@@ -4,6 +4,7 @@ import { SharedModule } from "../shared/shared.module";
import { isNotEmpty } from "../shared/empty.util"; import { isNotEmpty } from "../shared/empty.util";
import { DSpaceRESTv2Service } from "./dspace-rest-v2/dspace-rest-v2.service"; import { DSpaceRESTv2Service } from "./dspace-rest-v2/dspace-rest-v2.service";
import { CollectionDataService } from "./data-services/collection/collection-data.service"; import { CollectionDataService } from "./data-services/collection/collection-data.service";
import { CacheService } from "./data-services/cache/cache.service";
const IMPORTS = [ const IMPORTS = [
CommonModule, CommonModule,
@@ -18,7 +19,8 @@ const EXPORTS = [
const PROVIDERS = [ const PROVIDERS = [
CollectionDataService, CollectionDataService,
DSpaceRESTv2Service DSpaceRESTv2Service,
CacheService
]; ];
@NgModule({ @NgModule({

View File

@@ -3,13 +3,16 @@ import {
CollectionDataState, CollectionDataState,
collectionDataReducer collectionDataReducer
} from "./data-services/collection/collection-data.reducer"; } from "./data-services/collection/collection-data.reducer";
import { CacheState, cacheReducer } from "./data-services/cache/cache.reducer";
export interface CoreState { export interface CoreState {
collectionData: CollectionDataState collectionData: CollectionDataState,
cache: CacheState
} }
export const reducers = { export const reducers = {
collectionData: collectionDataReducer, collectionData: collectionDataReducer,
cache: cacheReducer
}; };
export function coreReducer(state: any, action: any) { export function coreReducer(state: any, action: any) {

View File

@@ -0,0 +1,33 @@
import { Action } from "@ngrx/store";
import { type } from "../../../shared/ngrx/type";
import { CacheableObject } from "./cache.reducer";
export const CacheActionTypes = {
ADD: type('dspace/core/data/cache/ADD'),
REMOVE: type('dspace/core/data/cache/REMOVE')
};
export class AddToCacheAction implements Action {
type = CacheActionTypes.ADD;
payload: {
objectToCache: CacheableObject;
msToLive: number;
};
constructor(objectToCache: CacheableObject, msToLive: number) {
this.payload = { objectToCache, msToLive };
}
}
export class RemoveFromCacheAction implements Action {
type = CacheActionTypes.REMOVE;
payload: string;
constructor(uuid: string) {
this.payload = uuid;
}
}
export type CacheAction
= AddToCacheAction
| RemoveFromCacheAction

View File

@@ -0,0 +1,58 @@
import { CacheAction, CacheActionTypes, AddToCacheAction, RemoveFromCacheAction } from "./cache.actions";
import { hasValue } from "../../../shared/empty.util";
export interface CacheableObject {
uuid: string;
}
export interface CacheEntry {
data: CacheableObject;
timeAdded: number;
msToLive: number;
}
export interface CacheState {
[uuid: string]: CacheEntry
}
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
const initialState: CacheState = Object.create(null);
export const cacheReducer = (state = initialState, action: CacheAction): CacheState => {
switch (action.type) {
case CacheActionTypes.ADD: {
return addToCache(state, <AddToCacheAction>action);
}
case CacheActionTypes.REMOVE: {
return removeFromCache(state, <RemoveFromCacheAction>action)
}
default: {
return state;
}
}
};
function addToCache(state: CacheState, action: AddToCacheAction): CacheState {
return Object.assign({}, state, {
[action.payload.objectToCache.uuid]: {
data: action.payload.objectToCache,
timeAdded: new Date().getTime(),
msToLive: action.payload.msToLive
}
});
}
function removeFromCache(state: CacheState, action: RemoveFromCacheAction): CacheState {
if (hasValue(state[action.payload])) {
let newCache = Object.assign({}, state);
delete newCache[action.payload];
return newCache;
}
else {
return state;
}
}

View File

@@ -0,0 +1,58 @@
import { Injectable } from "@angular/core";
import { Store } from "@ngrx/store";
import { CacheState, CacheEntry, CacheableObject } from "./cache.reducer";
import { AddToCacheAction, RemoveFromCacheAction } from "./cache.actions";
import { Observable } from "rxjs";
import { hasNoValue } from "../../../shared/empty.util";
@Injectable()
export class CacheService {
constructor(
private store: Store<CacheState>
) {}
add(objectToCache: CacheableObject, msToLive: number): void {
this.store.dispatch(new AddToCacheAction(objectToCache, msToLive));
}
remove(uuid: string): void {
this.store.dispatch(new RemoveFromCacheAction(uuid));
}
get<T extends CacheableObject>(uuid: string): Observable<T> {
return this.store.select<CacheEntry>('core', 'cache', uuid)
.filter(entry => this.isValid(entry))
.map((entry: CacheEntry) => <T> entry.data);
}
getList<T extends CacheableObject>(uuids: Array<string>): Observable<Array<T>> {
return Observable.combineLatest(
uuids.map((id: string) => this.get<T>(id))
);
}
has(uuid: string): boolean {
let result: boolean;
this.store.select<CacheEntry>('core', 'cache', uuid)
.take(1)
.subscribe(entry => result = this.isValid(entry));
return result;
}
private isValid(entry: CacheEntry): 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 RemoveFromCacheAction(entry.data.uuid));
}
return !isOutDated;
}
}
}

View File

@@ -1,5 +1,5 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Actions, Effect } from "@ngrx/effects"; import { Actions, Effect, toPayload } from "@ngrx/effects";
import { Collection } from "../../shared/collection.model"; import { Collection } from "../../shared/collection.model";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { import {
@@ -15,33 +15,52 @@ import {
import { DSpaceRESTV2Response } from "../../dspace-rest-v2/dspace-rest-v2-response.model"; import { DSpaceRESTV2Response } from "../../dspace-rest-v2/dspace-rest-v2-response.model";
import { DSpaceRESTv2Serializer } from "../../dspace-rest-v2/dspace-rest-v2.serializer"; import { DSpaceRESTv2Serializer } from "../../dspace-rest-v2/dspace-rest-v2.serializer";
import { DSpaceRESTv2Service } from "../../dspace-rest-v2/dspace-rest-v2.service"; import { DSpaceRESTv2Service } from "../../dspace-rest-v2/dspace-rest-v2.service";
import { CacheService } from "../cache/cache.service";
import { GlobalConfig } from "../../../../config";
@Injectable() @Injectable()
export class CollectionDataEffects { export class CollectionDataEffects {
constructor( constructor(
private actions$: Actions, private actions$: Actions,
private restApiService: DSpaceRESTv2Service private restApi: DSpaceRESTv2Service,
private cache: CacheService
) {} ) {}
// 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.
@Effect() findAll$ = this.actions$ @Effect() findAll$ = this.actions$
.ofType(CollectionFindMultipleActionTypes.FIND_MULTI_REQUEST) .ofType(CollectionFindMultipleActionTypes.FIND_MULTI_REQUEST)
.switchMap(() => { .switchMap(() => {
return this.restApiService.get('/collections') return this.restApi.get('/collections')
.map((data: DSpaceRESTV2Response) => new DSpaceRESTv2Serializer(Collection).deserializeArray(data)) .map((data: DSpaceRESTV2Response) => new DSpaceRESTv2Serializer(Collection).deserializeArray(data))
.map((collections: Collection[]) => new CollectionFindMultipleSuccessAction(collections)) .do((collections: Collection[]) => {
collections.forEach((collection) => {
this.cache.add(collection, GlobalConfig.cache.msToLive);
});
})
.map((collections: Array<Collection>) => collections.map(collection => collection.id))
.map((ids: Array<string>) => new CollectionFindMultipleSuccessAction(ids))
.catch((errorMsg: string) => Observable.of(new CollectionFindMultipleErrorAction(errorMsg))); .catch((errorMsg: string) => Observable.of(new CollectionFindMultipleErrorAction(errorMsg)));
}); });
@Effect() findById$ = this.actions$ @Effect() findById$ = this.actions$
.ofType(CollectionFindSingleActionTypes.FIND_BY_ID_REQUEST) .ofType(CollectionFindSingleActionTypes.FIND_BY_ID_REQUEST)
.switchMap(action => { .switchMap(action => {
return this.restApiService.get(`/collections/${action.payload}`) if (this.cache.has(action.payload)) {
.map((data: DSpaceRESTV2Response) => { return this.cache.get<Collection>(action.payload)
const t = new DSpaceRESTv2Serializer(Collection).deserialize(data); .map(collection => new CollectionFindByIdSuccessAction(collection.id));
return t; }
}) else {
.map((collection: Collection) => new CollectionFindByIdSuccessAction(collection)) return this.restApi.get(`/collections/${action.payload}`)
.catch((errorMsg: string) => Observable.of(new CollectionFindByIdErrorAction(errorMsg))); .map((data: DSpaceRESTV2Response) => new DSpaceRESTv2Serializer(Collection).deserialize(data))
}); .do((collection: Collection) => {
this.cache.add(collection, GlobalConfig.cache.msToLive);
})
.map((collection: Collection) => new CollectionFindByIdSuccessAction(collection.id))
.catch((errorMsg: string) => Observable.of(new CollectionFindByIdErrorAction(errorMsg)));
}
});
} }

View File

@@ -5,26 +5,29 @@ import { Store } from "@ngrx/store";
import { Collection } from "../../shared/collection.model"; import { Collection } from "../../shared/collection.model";
import { CollectionFindMultipleRequestAction } from "./collection-find-multiple.actions"; import { CollectionFindMultipleRequestAction } from "./collection-find-multiple.actions";
import { CollectionFindByIdRequestAction } from "./collection-find-single.actions"; import { CollectionFindByIdRequestAction } from "./collection-find-single.actions";
import { isNotEmpty } from "../../../shared/empty.util"; import { CacheService } from "../cache/cache.service";
import 'rxjs/add/operator/filter'; import 'rxjs/add/observable/forkJoin';
@Injectable() @Injectable()
export class CollectionDataService { export class CollectionDataService {
constructor( constructor(
private store: Store<CollectionDataState> private store: Store<CollectionDataState>,
private cache: CacheService
) { } ) { }
findAll(scope?: Collection): Observable<Collection[]> { findAll(scope?: Collection): Observable<Collection[]> {
this.store.dispatch(new CollectionFindMultipleRequestAction(scope)); this.store.dispatch(new CollectionFindMultipleRequestAction(scope));
return this.store.select<Collection[]>('core', 'collectionData', 'findMultiple', 'collections'); //get an observable of the IDs from the collectionData store
return this.store.select<Array<string>>('core', 'collectionData', 'findMultiple', 'collectionsIDs')
.flatMap((collectionIds: Array<string>) => {
// use those IDs to fetch the actual collection objects from the cache
return this.cache.getList<Collection>(collectionIds);
});
} }
findById(id: string): Observable<Collection> { findById(id: string): Observable<Collection> {
this.store.dispatch(new CollectionFindByIdRequestAction(id)); this.store.dispatch(new CollectionFindByIdRequestAction(id));
return this.store.select<Collection>('core', 'collectionData', 'findSingle', 'collection') return this.cache.get<Collection>(id);
//this filter is necessary because the same collection
//object in the state is used for every findById call
.filter(collection => isNotEmpty(collection) && collection.id === id);
} }
} }

View File

@@ -33,10 +33,10 @@ export class CollectionFindMultipleRequestAction implements Action {
export class CollectionFindMultipleSuccessAction implements Action { export class CollectionFindMultipleSuccessAction implements Action {
type = CollectionFindMultipleActionTypes.FIND_MULTI_SUCCESS; type = CollectionFindMultipleActionTypes.FIND_MULTI_SUCCESS;
payload: Collection[]; payload: Array<string>;
constructor(collections: Collection[]) { constructor(collectionIDs: Array<string>) {
this.payload = collections; this.payload = collectionIDs;
} }
} }

View File

@@ -8,7 +8,7 @@ import {
export interface CollectionFindMultipleState { export interface CollectionFindMultipleState {
scope: Collection; scope: Collection;
collections: Collection[]; collectionsIDs: Array<String>;
isLoading: boolean; isLoading: boolean;
errorMessage: string; errorMessage: string;
paginationOptions: PaginationOptions; paginationOptions: PaginationOptions;
@@ -17,7 +17,7 @@ export interface CollectionFindMultipleState {
const initialState: CollectionFindMultipleState = { const initialState: CollectionFindMultipleState = {
scope: undefined, scope: undefined,
collections: [], collectionsIDs: [],
isLoading: false, isLoading: false,
errorMessage: undefined, errorMessage: undefined,
paginationOptions: undefined, paginationOptions: undefined,
@@ -30,7 +30,7 @@ export const findMultipleReducer = (state = initialState, action: CollectionFind
case CollectionFindMultipleActionTypes.FIND_MULTI_REQUEST: { case CollectionFindMultipleActionTypes.FIND_MULTI_REQUEST: {
return Object.assign({}, state, { return Object.assign({}, state, {
scope: action.payload.scope, scope: action.payload.scope,
collections: [], collectionsIDs: [],
isLoading: true, isLoading: true,
errorMessage: undefined, errorMessage: undefined,
paginationOptions: action.payload.paginationOptions, paginationOptions: action.payload.paginationOptions,
@@ -41,7 +41,7 @@ export const findMultipleReducer = (state = initialState, action: CollectionFind
case CollectionFindMultipleActionTypes.FIND_MULTI_SUCCESS: { case CollectionFindMultipleActionTypes.FIND_MULTI_SUCCESS: {
return Object.assign({}, state, { return Object.assign({}, state, {
isLoading: false, isLoading: false,
collections: action.payload, collectionsIDs: action.payload,
errorMessage: undefined errorMessage: undefined
}); });
} }

View File

@@ -19,10 +19,10 @@ export class CollectionFindByIdRequestAction implements Action {
export class CollectionFindByIdSuccessAction implements Action { export class CollectionFindByIdSuccessAction implements Action {
type = CollectionFindSingleActionTypes.FIND_BY_ID_SUCCESS; type = CollectionFindSingleActionTypes.FIND_BY_ID_SUCCESS;
payload: Collection; payload: string;
constructor(collection: Collection) { constructor(collectionID: string) {
this.payload = collection; this.payload = collectionID;
} }
} }

View File

@@ -5,17 +5,15 @@ import {
} from "./collection-find-single.actions"; } from "./collection-find-single.actions";
export interface CollectionFindSingleState { export interface CollectionFindSingleState {
collection: Collection;
isLoading: boolean; isLoading: boolean;
errorMessage: string; errorMessage: string;
id: string; collectionID: string;
} }
const initialState: CollectionFindSingleState = { const initialState: CollectionFindSingleState = {
collection: undefined,
isLoading: false, isLoading: false,
errorMessage: undefined, errorMessage: undefined,
id: undefined, collectionID: undefined
}; };
export const findSingleReducer = (state = initialState, action: CollectionFindSingleAction): CollectionFindSingleState => { export const findSingleReducer = (state = initialState, action: CollectionFindSingleAction): CollectionFindSingleState => {
@@ -24,17 +22,15 @@ export const findSingleReducer = (state = initialState, action: CollectionFindSi
case CollectionFindSingleActionTypes.FIND_BY_ID_REQUEST: { case CollectionFindSingleActionTypes.FIND_BY_ID_REQUEST: {
return Object.assign({}, state, { return Object.assign({}, state, {
isLoading: true, isLoading: true,
id: action.payload,
collections: undefined,
errorMessage: undefined, errorMessage: undefined,
collectionID: action.payload
}); });
} }
case CollectionFindSingleActionTypes.FIND_BY_ID_SUCCESS: { case CollectionFindSingleActionTypes.FIND_BY_ID_SUCCESS: {
return Object.assign({}, state, { return Object.assign({}, state, {
isLoading: false, isLoading: false,
collection: action.payload, errorMessage: undefined,
errorMessage: undefined
}); });
} }

View File

@@ -1,11 +1,12 @@
import { autoserialize, autoserializeAs } from "cerialize"; import { autoserialize, autoserializeAs } from "cerialize";
import { Metadatum } from "./metadatum.model" import { Metadatum } from "./metadatum.model"
import { isEmpty, isNotEmpty } from "../../shared/empty.util"; import { isEmpty, isNotEmpty } from "../../shared/empty.util";
import { CacheableObject } from "../data-services/cache/cache.reducer";
/** /**
* An abstract model class for a DSpaceObject. * An abstract model class for a DSpaceObject.
*/ */
export abstract class DSpaceObject { export abstract class DSpaceObject implements CacheableObject {
/** /**
* The identifier of this DSpaceObject * The identifier of this DSpaceObject
@@ -64,4 +65,12 @@ export abstract class DSpaceObject {
return undefined; return undefined;
} }
} }
get uuid(): string {
return this.id;
}
set uuid(val: string) {
this.id = val;
}
} }

View File

@@ -1,8 +1,8 @@
import { Inject, Injectable, isDevMode } from '@angular/core'; import { Inject, Injectable, isDevMode } from '@angular/core';
@Injectable() @Injectable()
export class CacheService { export class DemoCacheService {
static KEY = 'CacheService'; static KEY = 'DemoCacheService';
constructor( @Inject('LRU') public _cache: Map<string, any>) { constructor( @Inject('LRU') public _cache: Map<string, any>) {
@@ -71,7 +71,7 @@ export class CacheService {
*/ */
normalizeKey(key: string | number): string { normalizeKey(key: string | number): string {
if (isDevMode() && this._isInvalidValue(key)) { if (isDevMode() && this._isInvalidValue(key)) {
throw new Error('Please provide a valid key to save in the CacheService'); throw new Error('Please provide a valid key to save in the DemoCacheService');
} }
return key + ''; return key + '';

View File

@@ -4,7 +4,7 @@ import 'rxjs/add/observable/of';
import 'rxjs/add/operator/do'; import 'rxjs/add/operator/do';
import 'rxjs/add/operator/share'; import 'rxjs/add/operator/share';
import { CacheService } from '../cache.service'; import { DemoCacheService } from '../demo-cache.service';
import { ApiService } from '../api.service'; import { ApiService } from '../api.service';
export function hashCodeString(str: string): string { export function hashCodeString(str: string): string {
@@ -24,7 +24,7 @@ export function hashCodeString(str: string): string {
@Injectable() @Injectable()
export class ModelService { export class ModelService {
// This is only one example of one Model depending on your domain // This is only one example of one Model depending on your domain
constructor(public _api: ApiService, public _cache: CacheService) { constructor(public _api: ApiService, public _cache: DemoCacheService) {
} }

View File

@@ -92,7 +92,7 @@ export function createMockApi() {
router.route('/collections/:collection_id') router.route('/collections/:collection_id')
.get(function(req, res) { .get(function(req, res) {
console.log('GET', util.inspect(req.collection, { colors: true })); console.log('GET', util.inspect(req.collection.id, { colors: true }));
res.json(toHALResponse(req, req.collection)); res.json(toHALResponse(req, req.collection));
// }) // })
// .put(function(req, res) { // .put(function(req, res) {

View File

@@ -10,7 +10,7 @@ import { TranslateLoader, TranslateModule, TranslateStaticLoader } from 'ng2-tra
import { AppModule, AppComponent } from './app/app.module'; import { AppModule, AppComponent } from './app/app.module';
import { SharedModule } from './app/shared/shared.module'; import { SharedModule } from './app/shared/shared.module';
import { CacheService } from './app/shared/cache.service'; import { DemoCacheService } from './app/shared/demo-cache.service';
import { CoreModule } from "./app/core/core.module"; import { CoreModule } from "./app/core/core.module";
// Will be merged into @angular/platform-browser in a later release // Will be merged into @angular/platform-browser in a later release
@@ -70,7 +70,7 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE';
{ provide: 'LRU', useFactory: getLRU, deps: [] }, { provide: 'LRU', useFactory: getLRU, deps: [] },
CacheService, DemoCacheService,
Meta, Meta,
@@ -78,14 +78,14 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE';
] ]
}) })
export class MainModule { export class MainModule {
constructor(public cache: CacheService) { constructor(public cache: DemoCacheService) {
// TODO(gdi2290): refactor into a lifecycle hook // TODO(gdi2290): refactor into a lifecycle hook
this.doRehydrate(); this.doRehydrate();
} }
doRehydrate() { doRehydrate() {
let defaultValue = {}; let defaultValue = {};
let serverCache = this._getCacheValue(CacheService.KEY, defaultValue); let serverCache = this._getCacheValue(DemoCacheService.KEY, defaultValue);
this.cache.rehydrate(serverCache); this.cache.rehydrate(serverCache);
} }

View File

@@ -9,7 +9,7 @@ import { TranslateLoader, TranslateModule, TranslateStaticLoader } from 'ng2-tra
import { AppModule, AppComponent } from './app/app.module'; import { AppModule, AppComponent } from './app/app.module';
import { SharedModule } from './app/shared/shared.module'; import { SharedModule } from './app/shared/shared.module';
import { CacheService } from './app/shared/cache.service'; import { DemoCacheService } from './app/shared/demo-cache.service';
import { CoreModule } from "./app/core/core.module"; import { CoreModule } from "./app/core/core.module";
// Will be merged into @angular/platform-browser in a later release // Will be merged into @angular/platform-browser in a later release
@@ -61,13 +61,13 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE';
{ provide: 'LRU', useFactory: getLRU, deps: [] }, { provide: 'LRU', useFactory: getLRU, deps: [] },
CacheService, DemoCacheService,
Meta, Meta,
] ]
}) })
export class MainModule { export class MainModule {
constructor(public cache: CacheService) { constructor(public cache: DemoCacheService) {
} }
@@ -76,7 +76,7 @@ export class MainModule {
* in Universal for now until it's fixed * in Universal for now until it's fixed
*/ */
universalDoDehydrate = (universalCache) => { universalDoDehydrate = (universalCache) => {
universalCache[CacheService.KEY] = JSON.stringify(this.cache.dehydrate()); universalCache[DemoCacheService.KEY] = JSON.stringify(this.cache.dehydrate());
} }
/** /**