implementation of server synchronization

This commit is contained in:
lotte
2018-09-14 09:26:30 +02:00
parent acb5f5197d
commit 17ad62c172
11 changed files with 378 additions and 48 deletions

View File

@@ -20,7 +20,12 @@ module.exports = {
// NOTE: how long should objects be cached for by default
msToLive: 15 * 60 * 1000, // 15 minutes
// msToLive: 1000, // 15 minutes
control: 'max-age=60' // revalidate browser
control: 'max-age=60', // revalidate browser
autoSync: {
defaultTime: 0,
maxBufferSize: 100,
timePerMethod: {'Patch': 30} //time in seconds
}
},
// Form settings
form: {

View File

@@ -11,7 +11,8 @@ export const ObjectCacheActionTypes = {
ADD: type('dspace/core/cache/object/ADD'),
REMOVE: type('dspace/core/cache/object/REMOVE'),
RESET_TIMESTAMPS: type('dspace/core/cache/object/RESET_TIMESTAMPS'),
PATCH: type('dspace/core/cache/object/PATCH')
ADD_PATCH: type('dspace/core/cache/object/ADD_PATCH'),
APPLY_PATCH: type('dspace/core/cache/object/APPLY_PATCH')
};
/* tslint:disable:max-classes-per-file */
@@ -56,11 +57,11 @@ export class RemoveFromObjectCacheAction implements Action {
/**
* Create a new RemoveFromObjectCacheAction
*
* @param uuid
* the UUID of the object to remove
* @param href
* the unique href of the object to remove
*/
constructor(uuid: string) {
this.payload = uuid;
constructor(href: string) {
this.payload = href;
}
}
@@ -83,25 +84,43 @@ export class ResetObjectCacheTimestampsAction implements Action {
}
/**
* An ngrx action to add new operations to a specified cached objects
* An ngrx action to add new operations to a specified cached object
*/
export class PatchObjectCacheAction implements Action {
type = ObjectCacheActionTypes.PATCH;
export class AddPatchObjectCacheAction implements Action {
type = ObjectCacheActionTypes.ADD_PATCH;
payload: {
uuid: string,
href: string,
operations: Operation[]
};
/**
* Create a new PatchObjectCacheAction
* Create a new AddPatchObjectCacheAction
*
* @param uuid
* the uuid of the object that should be updated
* @param href
* the unique href of the object that should be updated
* @param operations
* the list of operations to add
*/
constructor(uuid: string, operations: Operation[]) {
this.payload = { uuid, operations };
constructor(href: string, operations: Operation[]) {
this.payload = { href, operations };
}
}
/**
* An ngrx action to apply all existing operations to a specified cached object
*/
export class ApplyPatchObjectCacheAction implements Action {
type = ObjectCacheActionTypes.APPLY_PATCH;
payload: string;
/**
* Create a new ApplyPatchObjectCacheAction
*
* @param href
* the unique href of the object that should be updated
*/
constructor(href: string) {
this.payload = href;
}
}
@@ -114,4 +133,5 @@ export type ObjectCacheAction
= AddToObjectCacheAction
| RemoveFromObjectCacheAction
| ResetObjectCacheTimestampsAction
| PatchObjectCacheAction;
| AddPatchObjectCacheAction
| ApplyPatchObjectCacheAction;

View File

@@ -1,11 +1,15 @@
import {
ObjectCacheAction, ObjectCacheActionTypes, AddToObjectCacheAction,
RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction, PatchObjectCacheAction
ObjectCacheAction,
ObjectCacheActionTypes,
AddToObjectCacheAction,
RemoveFromObjectCacheAction,
ResetObjectCacheTimestampsAction,
AddPatchObjectCacheAction, ApplyPatchObjectCacheAction
} from './object-cache.actions';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { CacheEntry } from './cache-entry';
import { ResourceType } from '../shared/resource-type';
import { Operation } from 'fast-json-patch';
import { applyPatch, Operation } from 'fast-json-patch';
export enum DirtyType {
Created = 'Created',
@@ -13,7 +17,11 @@ export enum DirtyType {
Deleted = 'Deleted'
}
/**
export interface Patch {
uuid?: string;
operations: Operation[];
}
/**conca
* An interface to represent objects that can be cached
*
* A cacheable object should have a self link
@@ -37,7 +45,8 @@ export class ObjectCacheEntry implements CacheEntry {
timeAdded: number;
msToLive: number;
requestHref: string;
operations: Operation[];
patches: Patch[] = [];
isDirty: boolean;
}
/**
@@ -78,9 +87,14 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi
return resetObjectCacheTimestamps(state, action as ResetObjectCacheTimestampsAction)
}
case ObjectCacheActionTypes.PATCH: {
return patchObjectCache(state, action as PatchObjectCacheAction);
case ObjectCacheActionTypes.ADD_PATCH: {
return addPatchObjectCache(state, action as AddPatchObjectCacheAction);
}
case ObjectCacheActionTypes.APPLY_PATCH: {
return applyPatchObjectCache(state, action as ApplyPatchObjectCacheAction);
}
default: {
return state;
}
@@ -104,7 +118,7 @@ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheActio
timeAdded: action.payload.timeAdded,
msToLive: action.payload.msToLive,
requestHref: action.payload.requestHref,
operations: []
isDirty: false
}
});
}
@@ -156,16 +170,41 @@ function resetObjectCacheTimestamps(state: ObjectCacheState, action: ResetObject
* @param state
* the current state
* @param action
* a PatchObjectCacheAction
* an AddPatchObjectCacheAction
* @return ObjectCacheState
* the new state, with the new operations added to the state of the specified ObjectCacheEntry
*/
function patchObjectCache(state: ObjectCacheState, action: PatchObjectCacheAction): ObjectCacheState {
const uuid = action.payload.uuid;
function addPatchObjectCache(state: ObjectCacheState, action: AddPatchObjectCacheAction): ObjectCacheState {
const uuid = action.payload.href;
const operations = action.payload.operations;
const newState = Object.assign({}, state);
if (hasValue(newState[uuid])) {
newState[uuid].operations = state[uuid].operations.concat(operations);
const patches = newState[uuid].patches;
newState[uuid].patches = [...patches, {operations} as Patch];
newState[uuid].isDirty = true;
}
return newState;
}
/**
* Apply the list of patch operations to a cached object
*
* @param state
* the current state
* @param action
* an ApplyPatchObjectCacheAction
* @return ObjectCacheState
* the new state, with the new operations applied to the state of the specified ObjectCacheEntry
*/
function applyPatchObjectCache(state: ObjectCacheState, action: ApplyPatchObjectCacheAction): ObjectCacheState {
const uuid = action.payload;
const newState = Object.assign({}, state);
if (hasValue(newState[uuid])) {
// flatten two dimensional array
const flatPatch: Operation[] = [].concat(...newState[uuid].patches);
const newData = applyPatch( newState[uuid].data, flatPatch);
newState[uuid].data = newData.newDocument;
newState[uuid].patches = [];
}
return newState;
}

View File

@@ -7,8 +7,8 @@ import { IndexName } from '../index/index.reducer';
import { CacheableObject, ObjectCacheEntry } from './object-cache.reducer';
import {
AddToObjectCacheAction,
PatchObjectCacheAction,
AddPatchObjectCacheAction,
AddToObjectCacheAction, ApplyPatchObjectCacheAction,
RemoveFromObjectCacheAction
} from './object-cache.actions';
import { hasNoValue, isNotEmpty } from '../../shared/empty.util';
@@ -18,13 +18,15 @@ import { pathSelector } from '../shared/selectors';
import { NormalizedObjectFactory } from './models/normalized-object-factory';
import { NormalizedObject } from './models/normalized-object.model';
import { applyPatch, Operation } from 'fast-json-patch';
import { RestRequestMethod } from '../data/request.models';
import { AddToSSBAction } from './server-sync-buffer.actions';
function selfLinkFromUuidSelector(uuid: string): MemoizedSelector<CoreState, string> {
return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.OBJECT, uuid);
}
function entryFromSelfLinkSelector(selfLink: string): MemoizedSelector<CoreState, ObjectCacheEntry> {
return pathSelector<CoreState, ObjectCacheEntry>(coreSelector, 'data/object', selfLink);
return pathSelector<CoreState, ObjectCacheEntry>(coreSelector, 'cache/object', selfLink);
}
/**
@@ -52,10 +54,10 @@ export class ObjectCacheService {
}
/**
* Remove the object with the supplied UUID from the cache
* Remove the object with the supplied href from the cache
*
* @param uuid
* The UUID of the object to be removed
* @param href
* The unique href of the object to be removed
*/
remove(uuid: string): void {
this.store.dispatch(new RemoveFromObjectCacheAction(uuid));
@@ -87,13 +89,17 @@ export class ObjectCacheService {
getBySelfLink<T extends NormalizedObject>(selfLink: string): Observable<T> {
return this.getEntry(selfLink).pipe(
map((entry: ObjectCacheEntry) => {
// flatten two dimensional array
const flatPatch: Operation[] = [].concat(...entry.patches);
const patchedData = applyPatch(entry.data, flatPatch).newDocument;
return Object.assign({}, entry, { data: patchedData });
}
),
map((entry: ObjectCacheEntry) => {
const type: GenericConstructor<NormalizedObject> = NormalizedObjectFactory.getConstructor(entry.data.type);
return Object.assign(new type(), entry.data) as T
}),
// map((entry: ObjectCacheEntry) =>
// applyPatch(entry.data, entry.operations).newDocument
// )
})
);
}
@@ -205,14 +211,16 @@ export class ObjectCacheService {
}
/**
* Add operations to a the existing list of operations for an ObjectCacheEntry
* Add operations to the existing list of operations for an ObjectCacheEntry
* Makes sure the ServerSyncBuffer for this ObjectCacheEntry is updated
* @param {string} uuid
* the uuid of the ObjectCacheEntry
* @param {Operation[]} patch
* list of operations to perform
*/
private addOperations(uuid: string, patch: Operation[]) {
this.store.dispatch(new PatchObjectCacheAction(uuid, patch));
private addPatch(uuid: string, patch: Operation[]) {
this.store.dispatch(new AddPatchObjectCacheAction(uuid, patch));
this.store.dispatch(new AddToSSBAction(uuid, RestRequestMethod.Patch));
}
/**
@@ -224,6 +232,17 @@ export class ObjectCacheService {
* false if the entry is there are no operations left in the ObjectCacheEntry, true otherwise
*/
private isDirty(entry: ObjectCacheEntry): boolean {
return isNotEmpty(entry.operations);
return isNotEmpty(entry.patches);
}
/**
* Apply the existing operations on an ObjectCacheEntry in the store
* NB: this does not make any server side changes
* @param {string} uuid
* the uuid of the ObjectCacheEntry
*/
private applyPatchesToCachedObject(uuid: string) {
this.store.dispatch(new ApplyPatchObjectCacheAction(uuid));
}
}

View File

@@ -12,7 +12,7 @@ import { coreSelector, CoreState } from '../core.reducers';
import { pathSelector } from '../shared/selectors';
function entryFromKeySelector(key: string): MemoizedSelector<CoreState, ResponseCacheEntry> {
return pathSelector<CoreState, ResponseCacheEntry>(coreSelector, 'data/response', key);
return pathSelector<CoreState, ResponseCacheEntry>(coreSelector, 'cache/response', key);
}
/**

View File

@@ -0,0 +1,83 @@
import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type';
import { CacheableObject } from './object-cache.reducer';
import { Operation } from 'fast-json-patch';
import { RestRequest, RestRequestMethod } from '../data/request.models';
/**
* The list of ServerSyncBufferAction type definitions
*/
export const ServerSyncBufferActionTypes = {
ADD: type('dspace/core/cache/syncbuffer/ADD'),
COMMIT: type('dspace/core/cache/syncbuffer/COMMIT'),
EMPTY: type('dspace/core/cache/syncbuffer/EMPTY'),
};
/* tslint:disable:max-classes-per-file */
/**
* An ngrx action to add a new cached object to the server's sync buffer
*/
export class AddToSSBAction implements Action {
type = ServerSyncBufferActionTypes.ADD;
payload: {
href: string,
method: RestRequestMethod
};
/**
* Create a new AddToSSBAction
*
* @param href
* the unique href of the cached object entry that should be updated
*/
constructor(href: string, method: RestRequestMethod) {
this.payload = { href, method };
}
}
/**
* An ngrx action to commit everything (for a certain method, when specified) in the ServerSyncBuffer to the server
*/
export class CommitSSBAction implements Action {
type = ServerSyncBufferActionTypes.COMMIT;
payload?: RestRequestMethod;
/**
* Create a new CommitSSBAction
*
* @param method
* an optional method for which the ServerSyncBuffer should send its entries to the server
*/
constructor(method?: RestRequestMethod) {
this.payload = method;
}
}
/**
* An ngrx action to remove everything (for a certain method, when specified) from the ServerSyncBuffer to the server
*/
export class EmptySSBAction implements Action {
type = ServerSyncBufferActionTypes.EMPTY;
payload?: RestRequestMethod;
/**
* Create a new EmptySSBAction
*
* @param method
* an optional method for which the ServerSyncBuffer should remove its entries
*/
constructor(method?: RestRequestMethod) {
this.payload = method;
}
}
/* tslint:enable:max-classes-per-file */
/**
* A type to encompass all ServerSyncBufferActions
*/
export type ServerSyncBufferAction
= AddToSSBAction
| CommitSSBAction
| EmptySSBAction

View File

@@ -0,0 +1,55 @@
import { delay, exhaustMap, filter, first, map } from 'rxjs/operators';
import { Inject, Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import {
AddToSSBAction,
CommitSSBAction,
EmptySSBAction,
ServerSyncBufferActionTypes
} from './server-sync-buffer.actions';
import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface';
import { CoreState } from '../core.reducers';
import { select, Store } from '@ngrx/store';
import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer';
import { of as observableOf } from 'rxjs';
import { RequestService } from '../data/request.service';
import { PutRequest } from '../data/request.models';
@Injectable()
export class ObjectCacheEffects {
@Effect() setTimeoutForServerSync = this.actions$
.pipe(ofType(ServerSyncBufferActionTypes.ADD),
exhaustMap((action: AddToSSBAction) => {
const autoSyncConfig = this.EnvConfig.cache.autoSync;
const timeoutInSeconds = autoSyncConfig.timePerMethod[action.type] || autoSyncConfig.defaultTime;
return observableOf(new CommitSSBAction(action.payload.method)).pipe(delay( timeoutInSeconds * 1000))
})
);
@Effect() commitServerSyncBuffer = this.actions$
.pipe(ofType(ServerSyncBufferActionTypes.COMMIT),
map((action: CommitSSBAction) => {
this.store.pipe(
select(serverSyncBufferSelector),
first()
).subscribe((bufferState: ServerSyncBufferState) => {
bufferState.buffer
.filter((entry: ServerSyncBufferEntry) => entry.method === action.payload)
.forEach((entry: ServerSyncBufferEntry) => {
this.requestService.configure(new PutRequest(this.requestService.generateRequestId(), ,))
})
});
return new EmptySSBAction(action.payload);
})
);
constructor(private actions$: Actions,
private store: Store<CoreState>,
private requestService: RequestService,
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) {
}
}
export const serverSyncBufferSelector = (state: CoreState) => state['cache/syncbuffer'];

View File

@@ -0,0 +1,92 @@
import { RestRequestMethod } from '../data/request.models';
import { hasNoValue, hasValue } from '../../shared/empty.util';
import {
AddToSSBAction,
EmptySSBAction,
ServerSyncBufferAction,
ServerSyncBufferActionTypes
} from './server-sync-buffer.actions';
/**
* An entry in the ServerSyncBufferState
* href: unique href of an ObjectCacheEntry
* method: RestRequestMethod type
*/
export class ServerSyncBufferEntry {
href: string;
method: RestRequestMethod;
}
/**
* The ServerSyncBuffer State
*
* Consists list of ServerSyncBufferState
*/
export interface ServerSyncBufferState {
buffer: ServerSyncBufferEntry[];
}
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
const initialState: ServerSyncBufferState = { buffer: [] };
/**
* The ServerSyncBuffer Reducer
*
* @param state
* the current state
* @param action
* the action to perform on the state
* @return ServerSyncBufferState
* the new state
*/
export function serverSyncBufferReducer(state = initialState, action: ServerSyncBufferAction): ServerSyncBufferState {
switch (action.type) {
case ServerSyncBufferActionTypes.ADD: {
return addToServerSyncQueue(state, action as AddToSSBAction)
}
case ServerSyncBufferActionTypes.EMPTY: {
return emptyServerSyncQueue(state, action as EmptySSBAction);
}
default: {
return state;
}
}
}
/**
* Add a new entry to the buffer with a specified method
*
* @param state
* the current state
* @param action
* an AddToSSBAction
* @return ServerSyncBufferState
* the new state, with a new entry added to the buffer
*/
function addToServerSyncQueue(state: ServerSyncBufferState, action: AddToSSBAction): ServerSyncBufferState {
const actionEntry = action.payload as ServerSyncBufferEntry;
if (hasNoValue(state.buffer.find((entry) => entry.href === actionEntry.href && entry.method === actionEntry.method))) {
return Object.assign({}, state, { buffer: state.buffer.concat(actionEntry) });
}
}
/**
* Remove all ServerSyncBuffers entry from the buffer with a specified method
* If no method is specified, empty the whole buffer
*
* @param state
* the current state
* @param action
* an AddToSSBAction
* @return ServerSyncBufferState
* the new state, with a new entry added to the buffer
*/
function emptyServerSyncQueue(state: ServerSyncBufferState, action: EmptySSBAction): ServerSyncBufferState {
let newBuffer = [];
if (hasValue(action.payload)) {
newBuffer = state.buffer.filter((entry) => entry.method !== action.payload);
}
return Object.assign({}, state, { buffer: newBuffer });
}

View File

@@ -5,18 +5,21 @@ import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reduc
import { indexReducer, IndexState } from './index/index.reducer';
import { requestReducer, RequestState } from './data/request.reducer';
import { authReducer, AuthState } from './auth/auth.reducer';
import { serverSyncBufferReducer, ServerSyncBufferState } from './cache/server-sync-buffer.reducer';
export interface CoreState {
'data/object': ObjectCacheState,
'data/response': ResponseCacheState,
'cache/object': ObjectCacheState,
'cache/response': ResponseCacheState,
'cache/syncbuffer': ServerSyncBufferState,
'data/request': RequestState,
'index': IndexState,
'auth': AuthState,
}
export const coreReducers: ActionReducerMap<CoreState> = {
'data/object': objectCacheReducer,
'data/response': responseCacheReducer,
'cache/object': objectCacheReducer,
'cache/response': responseCacheReducer,
'cache/syncbuffer': serverSyncBufferReducer,
'data/request': requestReducer,
'index': indexReducer,
'auth': authReducer

View File

@@ -0,0 +1,12 @@
import { RestRequestMethod } from '../app/core/data/request.models';
/* enum indices */
type TimePerMethod = {
[method in RestRequestMethod]: number;
};
export interface AutoSyncConfig {
defaultTime: number;
timePerMethod: TimePerMethod;
maxBufferSize: number;
};

View File

@@ -1,6 +1,8 @@
import { Config } from './config.interface';
import { AutoSyncConfig } from './auto-sync-config.interface';
export interface CacheConfig extends Config {
msToLive: number,
control: string
control: string,
autoSync: AutoSyncConfig
}