mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
refactored requestCacheReducer
This commit is contained in:
6
src/app/core/cache/cache.reducers.ts
vendored
6
src/app/core/cache/cache.reducers.ts
vendored
@@ -1,14 +1,14 @@
|
|||||||
import { combineReducers } from "@ngrx/store";
|
import { combineReducers } from "@ngrx/store";
|
||||||
import { RequestCacheState, requestCacheReducer } from "./request-cache.reducer";
|
import { ResponseCacheState, responseCacheReducer } from "./response-cache.reducer";
|
||||||
import { ObjectCacheState, objectCacheReducer } from "./object-cache.reducer";
|
import { ObjectCacheState, objectCacheReducer } from "./object-cache.reducer";
|
||||||
|
|
||||||
export interface CacheState {
|
export interface CacheState {
|
||||||
request: RequestCacheState,
|
response: ResponseCacheState,
|
||||||
object: ObjectCacheState
|
object: ObjectCacheState
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reducers = {
|
export const reducers = {
|
||||||
request: requestCacheReducer,
|
response: responseCacheReducer,
|
||||||
object: objectCacheReducer
|
object: objectCacheReducer
|
||||||
};
|
};
|
||||||
|
|
||||||
|
1
src/app/core/cache/object-cache.reducer.ts
vendored
1
src/app/core/cache/object-cache.reducer.ts
vendored
@@ -12,6 +12,7 @@ import { CacheEntry } from "./cache-entry";
|
|||||||
*/
|
*/
|
||||||
export interface CacheableObject {
|
export interface CacheableObject {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
self?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
205
src/app/core/cache/request-cache.actions.ts
vendored
205
src/app/core/cache/request-cache.actions.ts
vendored
@@ -1,205 +0,0 @@
|
|||||||
import { OpaqueToken } from "@angular/core";
|
|
||||||
import { Action } from "@ngrx/store";
|
|
||||||
import { type } from "../../shared/ngrx/type";
|
|
||||||
import { PaginationOptions } from "../shared/pagination-options.model";
|
|
||||||
import { SortOptions } from "../shared/sort-options.model";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of RequestCacheAction type definitions
|
|
||||||
*/
|
|
||||||
export const RequestCacheActionTypes = {
|
|
||||||
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'),
|
|
||||||
REMOVE: type('dspace/core/cache/request/REMOVE'),
|
|
||||||
RESET_TIMESTAMPS: type('dspace/core/cache/request/RESET_TIMESTAMPS')
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An ngrx action to find all objects of a certain type
|
|
||||||
*/
|
|
||||||
export class RequestCacheFindAllAction implements Action {
|
|
||||||
type = RequestCacheActionTypes.FIND_ALL;
|
|
||||||
payload: {
|
|
||||||
key: string,
|
|
||||||
service: OpaqueToken,
|
|
||||||
scopeID: string,
|
|
||||||
paginationOptions: PaginationOptions,
|
|
||||||
sortOptions: SortOptions
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new RequestCacheFindAllAction
|
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* the key under which to cache this request, should be unique
|
|
||||||
* @param service
|
|
||||||
* the name of the service that initiated the action
|
|
||||||
* @param scopeID
|
|
||||||
* the id of an optional scope object
|
|
||||||
* @param paginationOptions
|
|
||||||
* the pagination options
|
|
||||||
* @param sortOptions
|
|
||||||
* the sort options
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
key: string,
|
|
||||||
service: OpaqueToken,
|
|
||||||
scopeID?: string,
|
|
||||||
paginationOptions: PaginationOptions = new PaginationOptions(),
|
|
||||||
sortOptions: SortOptions = new SortOptions()
|
|
||||||
) {
|
|
||||||
this.payload = {
|
|
||||||
key,
|
|
||||||
service,
|
|
||||||
scopeID,
|
|
||||||
paginationOptions,
|
|
||||||
sortOptions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An ngrx action to find objects by id
|
|
||||||
*/
|
|
||||||
export class RequestCacheFindByIDAction implements Action {
|
|
||||||
type = RequestCacheActionTypes.FIND_BY_ID;
|
|
||||||
payload: {
|
|
||||||
key: string,
|
|
||||||
service: OpaqueToken,
|
|
||||||
resourceID: string
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new RequestCacheFindByIDAction
|
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* the key under which to cache this request, should be unique
|
|
||||||
* @param service
|
|
||||||
* the name of the service that initiated the action
|
|
||||||
* @param resourceID
|
|
||||||
* the ID of the resource to find
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
key: string,
|
|
||||||
service: OpaqueToken,
|
|
||||||
resourceID: string
|
|
||||||
) {
|
|
||||||
this.payload = {
|
|
||||||
key,
|
|
||||||
service,
|
|
||||||
resourceID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An ngrx action to indicate a request was returned successful
|
|
||||||
*/
|
|
||||||
export class RequestCacheSuccessAction implements Action {
|
|
||||||
type = RequestCacheActionTypes.SUCCESS;
|
|
||||||
payload: {
|
|
||||||
key: string,
|
|
||||||
resourceUUIDs: Array<string>,
|
|
||||||
timeAdded: number,
|
|
||||||
msToLive: number
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new RequestCacheSuccessAction
|
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* the key under which cache this request is cached,
|
|
||||||
* should be identical to the one used in the corresponding
|
|
||||||
* find action
|
|
||||||
* @param resourceUUIDs
|
|
||||||
* the UUIDs returned from the backend
|
|
||||||
* @param timeAdded
|
|
||||||
* the time it was returned
|
|
||||||
* @param msToLive
|
|
||||||
* the amount of milliseconds before it should expire
|
|
||||||
*/
|
|
||||||
constructor(key: string, resourceUUIDs: Array<string>, timeAdded, msToLive: number) {
|
|
||||||
this.payload = {
|
|
||||||
key,
|
|
||||||
resourceUUIDs,
|
|
||||||
timeAdded,
|
|
||||||
msToLive
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An ngrx action to indicate a request failed
|
|
||||||
*/
|
|
||||||
export class RequestCacheErrorAction implements Action {
|
|
||||||
type = RequestCacheActionTypes.ERROR;
|
|
||||||
payload: {
|
|
||||||
key: string,
|
|
||||||
errorMessage: string
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new RequestCacheErrorAction
|
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* the key under which cache this request is cached,
|
|
||||||
* should be identical to the one used in the corresponding
|
|
||||||
* find action
|
|
||||||
* @param errorMessage
|
|
||||||
* A message describing the reason the request failed
|
|
||||||
*/
|
|
||||||
constructor(key: string, errorMessage: string) {
|
|
||||||
this.payload = {
|
|
||||||
key,
|
|
||||||
errorMessage
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An ngrx action to remove a request from the cache
|
|
||||||
*/
|
|
||||||
export class RequestCacheRemoveAction implements Action {
|
|
||||||
type = RequestCacheActionTypes.REMOVE;
|
|
||||||
payload: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new RequestCacheRemoveAction
|
|
||||||
* @param key
|
|
||||||
* The key of the request to remove
|
|
||||||
*/
|
|
||||||
constructor(key: string) {
|
|
||||||
this.payload = key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An ngrx action to reset the timeAdded property of all cached objects
|
|
||||||
*/
|
|
||||||
export class ResetRequestCacheTimestampsAction implements Action {
|
|
||||||
type = RequestCacheActionTypes.RESET_TIMESTAMPS;
|
|
||||||
payload: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new ResetObjectCacheTimestampsAction
|
|
||||||
*
|
|
||||||
* @param newTimestamp
|
|
||||||
* the new timeAdded all objects should get
|
|
||||||
*/
|
|
||||||
constructor(newTimestamp: number) {
|
|
||||||
this.payload = newTimestamp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A type to encompass all RequestCacheActions
|
|
||||||
*/
|
|
||||||
export type RequestCacheAction
|
|
||||||
= RequestCacheFindAllAction
|
|
||||||
| RequestCacheFindByIDAction
|
|
||||||
| RequestCacheSuccessAction
|
|
||||||
| RequestCacheErrorAction
|
|
||||||
| RequestCacheRemoveAction
|
|
||||||
| ResetRequestCacheTimestampsAction;
|
|
227
src/app/core/cache/request-cache.reducer.spec.ts
vendored
227
src/app/core/cache/request-cache.reducer.spec.ts
vendored
@@ -1,227 +0,0 @@
|
|||||||
import { requestCacheReducer, RequestCacheState } from "./request-cache.reducer";
|
|
||||||
import {
|
|
||||||
RequestCacheRemoveAction, RequestCacheFindByIDAction,
|
|
||||||
RequestCacheFindAllAction, RequestCacheSuccessAction, RequestCacheErrorAction,
|
|
||||||
ResetRequestCacheTimestampsAction
|
|
||||||
} from "./request-cache.actions";
|
|
||||||
import deepFreeze = require("deep-freeze");
|
|
||||||
import { OpaqueToken } from "@angular/core";
|
|
||||||
|
|
||||||
class NullAction extends RequestCacheRemoveAction {
|
|
||||||
type = null;
|
|
||||||
payload = null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("requestCacheReducer", () => {
|
|
||||||
const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"];
|
|
||||||
const services = [new OpaqueToken('service1'), new OpaqueToken('service2')];
|
|
||||||
const msToLive = 900000;
|
|
||||||
const uuids = [
|
|
||||||
"9e32a2e2-6b91-4236-a361-995ccdc14c60",
|
|
||||||
"598ce822-c357-46f3-ab70-63724d02d6ad",
|
|
||||||
"be8325f7-243b-49f4-8a4b-df2b793ff3b5"
|
|
||||||
];
|
|
||||||
const resourceID = "9978";
|
|
||||||
const paginationOptions = { "resultsPerPage": 10, "currentPage": 1 };
|
|
||||||
const sortOptions = { "field": "id", "direction": 0 };
|
|
||||||
const testState = {
|
|
||||||
[keys[0]]: {
|
|
||||||
"key": keys[0],
|
|
||||||
"service": services[0],
|
|
||||||
"resourceUUIDs": [uuids[0], uuids[1]],
|
|
||||||
"isLoading": false,
|
|
||||||
"paginationOptions": paginationOptions,
|
|
||||||
"sortOptions": sortOptions,
|
|
||||||
"timeAdded": new Date().getTime(),
|
|
||||||
"msToLive": msToLive
|
|
||||||
},
|
|
||||||
[keys[1]]: {
|
|
||||||
"key": keys[1],
|
|
||||||
"service": services[1],
|
|
||||||
"resourceID": resourceID,
|
|
||||||
"resourceUUIDs": [uuids[2]],
|
|
||||||
"isLoading": false,
|
|
||||||
"timeAdded": new Date().getTime(),
|
|
||||||
"msToLive": msToLive
|
|
||||||
}
|
|
||||||
};
|
|
||||||
deepFreeze(testState);
|
|
||||||
const errorState: {} = {
|
|
||||||
[keys[0]]: {
|
|
||||||
errorMessage: 'error',
|
|
||||||
resourceUUIDs: uuids
|
|
||||||
}
|
|
||||||
};
|
|
||||||
deepFreeze(errorState);
|
|
||||||
|
|
||||||
|
|
||||||
it("should return the current state when no valid actions have been made", () => {
|
|
||||||
const action = new NullAction();
|
|
||||||
const newState = requestCacheReducer(testState, action);
|
|
||||||
|
|
||||||
expect(newState).toEqual(testState);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should start with an empty cache", () => {
|
|
||||||
const action = new NullAction();
|
|
||||||
const initialState = requestCacheReducer(undefined, action);
|
|
||||||
|
|
||||||
expect(initialState).toEqual(Object.create(null));
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("FIND_BY_ID", () => {
|
|
||||||
const action = new RequestCacheFindByIDAction(keys[0], services[0], resourceID);
|
|
||||||
|
|
||||||
it("should perform the action without affecting the previous state", () => {
|
|
||||||
//testState has already been frozen above
|
|
||||||
requestCacheReducer(testState, action);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should add the request to the cache", () => {
|
|
||||||
const state = Object.create(null);
|
|
||||||
const newState = requestCacheReducer(state, action);
|
|
||||||
expect(newState[keys[0]].key).toBe(keys[0]);
|
|
||||||
expect(newState[keys[0]].service).toEqual(services[0]);
|
|
||||||
expect(newState[keys[0]].resourceID).toBe(resourceID);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should set isLoading to true", () => {
|
|
||||||
const state = Object.create(null);
|
|
||||||
const newState = requestCacheReducer(state, action);
|
|
||||||
expect(newState[keys[0]].isLoading).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove any previous error message or resourceUUID for the request", () => {
|
|
||||||
const newState = requestCacheReducer(errorState, action);
|
|
||||||
expect(newState[keys[0]].resourceUUIDs.length).toBe(0);
|
|
||||||
expect(newState[keys[0]].errorMessage).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("FIND_ALL", () => {
|
|
||||||
const action = new RequestCacheFindAllAction(keys[0], services[0], resourceID, paginationOptions, sortOptions);
|
|
||||||
|
|
||||||
it("should perform the action without affecting the previous state", () => {
|
|
||||||
//testState has already been frozen above
|
|
||||||
requestCacheReducer(testState, action);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should add the request to the cache", () => {
|
|
||||||
const state = Object.create(null);
|
|
||||||
const newState = requestCacheReducer(state, action);
|
|
||||||
expect(newState[keys[0]].key).toBe(keys[0]);
|
|
||||||
expect(newState[keys[0]].service).toEqual(services[0]);
|
|
||||||
expect(newState[keys[0]].scopeID).toBe(resourceID);
|
|
||||||
expect(newState[keys[0]].paginationOptions).toEqual(paginationOptions);
|
|
||||||
expect(newState[keys[0]].sortOptions).toEqual(sortOptions);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should set isLoading to true", () => {
|
|
||||||
const state = Object.create(null);
|
|
||||||
const newState = requestCacheReducer(state, action);
|
|
||||||
expect(newState[keys[0]].isLoading).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove any previous error message or resourceUUIDs for the request", () => {
|
|
||||||
const newState = requestCacheReducer(errorState, action);
|
|
||||||
expect(newState[keys[0]].resourceUUIDs.length).toBe(0);
|
|
||||||
expect(newState[keys[0]].errorMessage).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("SUCCESS", () => {
|
|
||||||
const successUUIDs = [uuids[0], uuids[2]];
|
|
||||||
const successTimeAdded = new Date().getTime();
|
|
||||||
const successMsToLive = 5;
|
|
||||||
const action = new RequestCacheSuccessAction(keys[0], successUUIDs, successTimeAdded, successMsToLive);
|
|
||||||
|
|
||||||
it("should perform the action without affecting the previous state", () => {
|
|
||||||
//testState has already been frozen above
|
|
||||||
requestCacheReducer(testState, action);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should add the response to the cached request", () => {
|
|
||||||
const newState = requestCacheReducer(testState, action);
|
|
||||||
expect(newState[keys[0]].resourceUUIDs).toBe(successUUIDs);
|
|
||||||
expect(newState[keys[0]].timeAdded).toBe(successTimeAdded);
|
|
||||||
expect(newState[keys[0]].msToLive).toBe(successMsToLive);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should set isLoading to false", () => {
|
|
||||||
const newState = requestCacheReducer(testState, action);
|
|
||||||
expect(newState[keys[0]].isLoading).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove any previous error message for the request", () => {
|
|
||||||
const newState = requestCacheReducer(errorState, action);
|
|
||||||
expect(newState[keys[0]].errorMessage).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("ERROR", () => {
|
|
||||||
const errorMsg = 'errorMsg';
|
|
||||||
const action = new RequestCacheErrorAction(keys[0], errorMsg);
|
|
||||||
|
|
||||||
it("should perform the action without affecting the previous state", () => {
|
|
||||||
//testState has already been frozen above
|
|
||||||
requestCacheReducer(testState, action);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should set an error message for the request", () => {
|
|
||||||
const newState = requestCacheReducer(errorState, action);
|
|
||||||
expect(newState[keys[0]].errorMessage).toBe(errorMsg);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should set isLoading to false", () => {
|
|
||||||
const newState = requestCacheReducer(testState, action);
|
|
||||||
expect(newState[keys[0]].isLoading).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("REMOVE", () => {
|
|
||||||
it("should perform the action without affecting the previous state", () => {
|
|
||||||
const action = new RequestCacheRemoveAction(keys[0]);
|
|
||||||
//testState has already been frozen above
|
|
||||||
requestCacheReducer(testState, action);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove the specified request from the cache", () => {
|
|
||||||
const action = new RequestCacheRemoveAction(keys[0]);
|
|
||||||
const newState = requestCacheReducer(testState, action);
|
|
||||||
expect(testState[keys[0]]).not.toBeUndefined();
|
|
||||||
expect(newState[keys[0]]).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shouldn't do anything when the specified key isn't cached", () => {
|
|
||||||
const wrongKey = "this isn't cached";
|
|
||||||
const action = new RequestCacheRemoveAction(wrongKey);
|
|
||||||
const newState = requestCacheReducer(testState, action);
|
|
||||||
expect(testState[wrongKey]).toBeUndefined();
|
|
||||||
expect(newState).toEqual(testState);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("RESET_TIMESTAMPS", () => {
|
|
||||||
const newTimeStamp = new Date().getTime();
|
|
||||||
const action = new ResetRequestCacheTimestampsAction(newTimeStamp);
|
|
||||||
|
|
||||||
it("should perform the action without affecting the previous state", () => {
|
|
||||||
//testState has already been frozen above
|
|
||||||
requestCacheReducer(testState, action);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should set the timestamp of all requests in the cache", () => {
|
|
||||||
const newState = requestCacheReducer(testState, action);
|
|
||||||
Object.keys(newState).forEach((key) => {
|
|
||||||
expect(newState[key].timeAdded).toEqual(newTimeStamp);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
});
|
|
212
src/app/core/cache/request-cache.reducer.ts
vendored
212
src/app/core/cache/request-cache.reducer.ts
vendored
@@ -1,212 +0,0 @@
|
|||||||
import { PaginationOptions } from "../shared/pagination-options.model";
|
|
||||||
import { SortOptions } from "../shared/sort-options.model";
|
|
||||||
import {
|
|
||||||
RequestCacheAction, RequestCacheActionTypes, RequestCacheFindAllAction,
|
|
||||||
RequestCacheSuccessAction, RequestCacheErrorAction, RequestCacheFindByIDAction,
|
|
||||||
RequestCacheRemoveAction, ResetRequestCacheTimestampsAction
|
|
||||||
} from "./request-cache.actions";
|
|
||||||
import { OpaqueToken } from "@angular/core";
|
|
||||||
import { CacheEntry } from "./cache-entry";
|
|
||||||
import { hasValue } from "../../shared/empty.util";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An entry in the RequestCache
|
|
||||||
*/
|
|
||||||
export class RequestCacheEntry implements CacheEntry {
|
|
||||||
service: OpaqueToken;
|
|
||||||
key: string;
|
|
||||||
scopeID: string;
|
|
||||||
resourceID: string;
|
|
||||||
resourceUUIDs: Array<String>;
|
|
||||||
resourceType: String;
|
|
||||||
isLoading: boolean;
|
|
||||||
errorMessage: string;
|
|
||||||
paginationOptions: PaginationOptions;
|
|
||||||
sortOptions: SortOptions;
|
|
||||||
timeAdded: number;
|
|
||||||
msToLive: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The RequestCache State
|
|
||||||
*/
|
|
||||||
export interface RequestCacheState {
|
|
||||||
[key: string]: RequestCacheEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
|
||||||
const initialState = Object.create(null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The RequestCache Reducer
|
|
||||||
*
|
|
||||||
* @param state
|
|
||||||
* the current state
|
|
||||||
* @param action
|
|
||||||
* the action to perform on the state
|
|
||||||
* @return RequestCacheState
|
|
||||||
* the new state
|
|
||||||
*/
|
|
||||||
export const requestCacheReducer = (state = initialState, action: RequestCacheAction): RequestCacheState => {
|
|
||||||
switch (action.type) {
|
|
||||||
|
|
||||||
case RequestCacheActionTypes.FIND_ALL: {
|
|
||||||
return findAllRequest(state, <RequestCacheFindAllAction> action);
|
|
||||||
}
|
|
||||||
|
|
||||||
case RequestCacheActionTypes.FIND_BY_ID: {
|
|
||||||
return findByIDRequest(state, <RequestCacheFindByIDAction> action);
|
|
||||||
}
|
|
||||||
|
|
||||||
case RequestCacheActionTypes.SUCCESS: {
|
|
||||||
return success(state, <RequestCacheSuccessAction> action);
|
|
||||||
}
|
|
||||||
|
|
||||||
case RequestCacheActionTypes.ERROR: {
|
|
||||||
return error(state, <RequestCacheErrorAction> action);
|
|
||||||
}
|
|
||||||
|
|
||||||
case RequestCacheActionTypes.REMOVE: {
|
|
||||||
return removeFromCache(state, <RequestCacheRemoveAction> action);
|
|
||||||
}
|
|
||||||
|
|
||||||
case RequestCacheActionTypes.RESET_TIMESTAMPS: {
|
|
||||||
return resetRequestCacheTimestamps(state, <ResetRequestCacheTimestampsAction>action)
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a FindAll request to the cache
|
|
||||||
*
|
|
||||||
* @param state
|
|
||||||
* the current state
|
|
||||||
* @param action
|
|
||||||
* a RequestCacheFindAllAction
|
|
||||||
* @return RequestCacheState
|
|
||||||
* the new state, with the request added, or overwritten
|
|
||||||
*/
|
|
||||||
function findAllRequest(state: RequestCacheState, action: RequestCacheFindAllAction): RequestCacheState {
|
|
||||||
return Object.assign({}, state, {
|
|
||||||
[action.payload.key]: {
|
|
||||||
key: action.payload.key,
|
|
||||||
service: action.payload.service,
|
|
||||||
scopeID: action.payload.scopeID,
|
|
||||||
resourceUUIDs: [],
|
|
||||||
isLoading: true,
|
|
||||||
errorMessage: undefined,
|
|
||||||
paginationOptions: action.payload.paginationOptions,
|
|
||||||
sortOptions: action.payload.sortOptions
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a FindByID request to the cache
|
|
||||||
*
|
|
||||||
* @param state
|
|
||||||
* the current state
|
|
||||||
* @param action
|
|
||||||
* a RequestCacheFindByIDAction
|
|
||||||
* @return RequestCacheState
|
|
||||||
* the new state, with the request added, or overwritten
|
|
||||||
*/
|
|
||||||
function findByIDRequest(state: RequestCacheState, action: RequestCacheFindByIDAction): RequestCacheState {
|
|
||||||
return Object.assign({}, state, {
|
|
||||||
[action.payload.key]: {
|
|
||||||
key: action.payload.key,
|
|
||||||
service: action.payload.service,
|
|
||||||
resourceID: action.payload.resourceID,
|
|
||||||
resourceUUIDs: [],
|
|
||||||
isLoading: true,
|
|
||||||
errorMessage: undefined,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a cached request with a successful response
|
|
||||||
*
|
|
||||||
* @param state
|
|
||||||
* the current state
|
|
||||||
* @param action
|
|
||||||
* a RequestCacheSuccessAction
|
|
||||||
* @return RequestCacheState
|
|
||||||
* the new state, with the response added to the request
|
|
||||||
*/
|
|
||||||
function success(state: RequestCacheState, action: RequestCacheSuccessAction): RequestCacheState {
|
|
||||||
return Object.assign({}, state, {
|
|
||||||
[action.payload.key]: Object.assign({}, state[action.payload.key], {
|
|
||||||
isLoading: false,
|
|
||||||
resourceUUIDs: action.payload.resourceUUIDs,
|
|
||||||
errorMessage: undefined,
|
|
||||||
timeAdded: action.payload.timeAdded,
|
|
||||||
msToLive: action.payload.msToLive
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a cached request with an error
|
|
||||||
*
|
|
||||||
* @param state
|
|
||||||
* the current state
|
|
||||||
* @param action
|
|
||||||
* a RequestCacheSuccessAction
|
|
||||||
* @return RequestCacheState
|
|
||||||
* the new state, with the error added to the request
|
|
||||||
*/
|
|
||||||
function error(state: RequestCacheState, action: RequestCacheErrorAction): RequestCacheState {
|
|
||||||
return Object.assign({}, state, {
|
|
||||||
[action.payload.key]: Object.assign({}, state[action.payload.key], {
|
|
||||||
isLoading: false,
|
|
||||||
errorMessage: action.payload.errorMessage
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a request from the cache
|
|
||||||
*
|
|
||||||
* @param state
|
|
||||||
* the current state
|
|
||||||
* @param action
|
|
||||||
* an RequestCacheRemoveAction
|
|
||||||
* @return RequestCacheState
|
|
||||||
* the new state, with the request removed if it existed.
|
|
||||||
*/
|
|
||||||
function removeFromCache(state: RequestCacheState, action: RequestCacheRemoveAction): RequestCacheState {
|
|
||||||
if (hasValue(state[action.payload])) {
|
|
||||||
let newCache = Object.assign({}, state);
|
|
||||||
delete newCache[action.payload];
|
|
||||||
|
|
||||||
return newCache;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the timeAdded timestamp of every cached request to the specified value
|
|
||||||
*
|
|
||||||
* @param state
|
|
||||||
* the current state
|
|
||||||
* @param action
|
|
||||||
* a ResetRequestCacheTimestampsAction
|
|
||||||
* @return RequestCacheState
|
|
||||||
* the new state, with all timeAdded timestamps set to the specified value
|
|
||||||
*/
|
|
||||||
function resetRequestCacheTimestamps(state: RequestCacheState, action: ResetRequestCacheTimestampsAction): RequestCacheState {
|
|
||||||
let newState = Object.create(null);
|
|
||||||
Object.keys(state).forEach(key => {
|
|
||||||
newState[key] = Object.assign({}, state[key], {
|
|
||||||
timeAdded: action.payload
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return newState;
|
|
||||||
}
|
|
147
src/app/core/cache/request-cache.service.spec.ts
vendored
147
src/app/core/cache/request-cache.service.spec.ts
vendored
@@ -1,147 +0,0 @@
|
|||||||
import { RequestCacheService } from "./request-cache.service";
|
|
||||||
import { Store } from "@ngrx/store";
|
|
||||||
import { RequestCacheState, RequestCacheEntry } from "./request-cache.reducer";
|
|
||||||
import { OpaqueToken } from "@angular/core";
|
|
||||||
import { RequestCacheFindAllAction, RequestCacheFindByIDAction } from "./request-cache.actions";
|
|
||||||
import { Observable } from "rxjs";
|
|
||||||
|
|
||||||
describe("RequestCacheService", () => {
|
|
||||||
let service: RequestCacheService;
|
|
||||||
let store: Store<RequestCacheState>;
|
|
||||||
|
|
||||||
const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"];
|
|
||||||
const serviceTokens = [new OpaqueToken('service1'), new OpaqueToken('service2')];
|
|
||||||
const resourceID = "9978";
|
|
||||||
const paginationOptions = { "resultsPerPage": 10, "currentPage": 1 };
|
|
||||||
const sortOptions = { "field": "id", "direction": 0 };
|
|
||||||
const timestamp = new Date().getTime();
|
|
||||||
const validCacheEntry = (key) => {
|
|
||||||
return {
|
|
||||||
key: key,
|
|
||||||
timeAdded: timestamp,
|
|
||||||
msToLive: 24 * 60 * 60 * 1000 // a day
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const invalidCacheEntry = (key) => {
|
|
||||||
return {
|
|
||||||
key: key,
|
|
||||||
timeAdded: 0,
|
|
||||||
msToLive: 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
store = new Store<RequestCacheState>(undefined, undefined, undefined);
|
|
||||||
spyOn(store, 'dispatch');
|
|
||||||
service = new RequestCacheService(store);
|
|
||||||
spyOn(window, 'Date').and.returnValue({ getTime: () => timestamp });
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("findAll", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(service, "get").and.callFake((key) => Observable.of({key: key}));
|
|
||||||
});
|
|
||||||
describe("if the key isn't cached", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(service, "has").and.returnValue(false);
|
|
||||||
});
|
|
||||||
it("should dispatch a FIND_ALL action with the key, service, scopeID, paginationOptions and sortOptions", () => {
|
|
||||||
service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions);
|
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(new RequestCacheFindAllAction(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions))
|
|
||||||
});
|
|
||||||
it("should return an observable of the newly cached request with the specified key", () => {
|
|
||||||
let result: RequestCacheEntry;
|
|
||||||
service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry);
|
|
||||||
expect(result.key).toEqual(keys[0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe("if the key is already cached", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(service, "has").and.returnValue(true);
|
|
||||||
});
|
|
||||||
it("shouldn't dispatch anything", () => {
|
|
||||||
service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions);
|
|
||||||
expect(store.dispatch).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
it("should return an observable of the existing cached request with the specified key", () => {
|
|
||||||
let result: RequestCacheEntry;
|
|
||||||
service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry);
|
|
||||||
expect(result.key).toEqual(keys[0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("findById", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(service, "get").and.callFake((key) => Observable.of({key: key}));
|
|
||||||
});
|
|
||||||
describe("if the key isn't cached", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(service, "has").and.returnValue(false);
|
|
||||||
});
|
|
||||||
it("should dispatch a FIND_BY_ID action with the key, service, and resourceID", () => {
|
|
||||||
service.findById(keys[0], serviceTokens[0], resourceID);
|
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(new RequestCacheFindByIDAction(keys[0], serviceTokens[0], resourceID))
|
|
||||||
});
|
|
||||||
it("should return an observable of the newly cached request with the specified key", () => {
|
|
||||||
let result: RequestCacheEntry;
|
|
||||||
service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry);
|
|
||||||
expect(result.key).toEqual(keys[0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe("if the key is already cached", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(service, "has").and.returnValue(true);
|
|
||||||
});
|
|
||||||
it("shouldn't dispatch anything", () => {
|
|
||||||
service.findById(keys[0], serviceTokens[0], resourceID);
|
|
||||||
expect(store.dispatch).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
it("should return an observable of the existing cached request with the specified key", () => {
|
|
||||||
let result: RequestCacheEntry;
|
|
||||||
service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry);
|
|
||||||
expect(result.key).toEqual(keys[0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("get", () => {
|
|
||||||
it("should return an observable of the cached request with the specified key", () => {
|
|
||||||
spyOn(store, "select").and.callFake((...args:Array<any>) => {
|
|
||||||
return Observable.of(validCacheEntry(args[args.length - 1]));
|
|
||||||
});
|
|
||||||
|
|
||||||
let testObj: RequestCacheEntry;
|
|
||||||
service.get(keys[1]).take(1).subscribe(entry => testObj = entry);
|
|
||||||
expect(testObj.key).toEqual(keys[1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not return a cached request that has exceeded its time to live", () => {
|
|
||||||
spyOn(store, "select").and.callFake((...args:Array<any>) => {
|
|
||||||
return Observable.of(invalidCacheEntry(args[args.length - 1]));
|
|
||||||
});
|
|
||||||
|
|
||||||
let getObsHasFired = false;
|
|
||||||
const subscription = service.get(keys[1]).subscribe(entry => getObsHasFired = true);
|
|
||||||
expect(getObsHasFired).toBe(false);
|
|
||||||
subscription.unsubscribe();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("has", () => {
|
|
||||||
it("should return true if the request with the supplied key is cached and still valid", () => {
|
|
||||||
spyOn(store, 'select').and.returnValue(Observable.of(validCacheEntry(keys[1])));
|
|
||||||
expect(service.has(keys[1])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false if the request with the supplied key isn't cached", () => {
|
|
||||||
spyOn(store, 'select').and.returnValue(Observable.of(undefined));
|
|
||||||
expect(service.has(keys[1])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false if the request with the supplied key is cached but has exceeded its time to live", () => {
|
|
||||||
spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry(keys[1])));
|
|
||||||
expect(service.has(keys[1])).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
138
src/app/core/cache/request-cache.service.ts
vendored
138
src/app/core/cache/request-cache.service.ts
vendored
@@ -1,138 +0,0 @@
|
|||||||
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";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A service to interact with the request cache
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class RequestCacheService {
|
|
||||||
constructor(
|
|
||||||
private store: Store<RequestCacheState>
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a new findAll request
|
|
||||||
*
|
|
||||||
* This will send a new findAll request to the backend,
|
|
||||||
* and store the request parameters and the fact that
|
|
||||||
* the request is pending
|
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* the key should be a unique identifier for the request and its parameters
|
|
||||||
* @param service
|
|
||||||
* the service that initiated the request
|
|
||||||
* @param scopeID
|
|
||||||
* the id of an optional scope object
|
|
||||||
* @param paginationOptions
|
|
||||||
* the pagination options (optional)
|
|
||||||
* @param sortOptions
|
|
||||||
* the sort options (optional)
|
|
||||||
* @return Observable<RequestCacheEntry>
|
|
||||||
* an observable of the RequestCacheEntry for this request
|
|
||||||
*/
|
|
||||||
findAll(
|
|
||||||
key: string,
|
|
||||||
service: OpaqueToken,
|
|
||||||
scopeID?: string,
|
|
||||||
paginationOptions?: PaginationOptions,
|
|
||||||
sortOptions?: SortOptions
|
|
||||||
): Observable<RequestCacheEntry> {
|
|
||||||
if (!this.has(key)) {
|
|
||||||
this.store.dispatch(new RequestCacheFindAllAction(key, service, scopeID, paginationOptions, sortOptions));
|
|
||||||
}
|
|
||||||
return this.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a new findById request
|
|
||||||
*
|
|
||||||
* This will send a new findById request to the backend,
|
|
||||||
* and store the request parameters and the fact that
|
|
||||||
* the request is pending
|
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* the key should be a unique identifier for the request and its parameters
|
|
||||||
* @param service
|
|
||||||
* the service that initiated the request
|
|
||||||
* @param resourceID
|
|
||||||
* the ID of the resource to find
|
|
||||||
* @return Observable<RequestCacheEntry>
|
|
||||||
* an observable of the RequestCacheEntry for this request
|
|
||||||
*/
|
|
||||||
findById(
|
|
||||||
key: string,
|
|
||||||
service: OpaqueToken,
|
|
||||||
resourceID: string
|
|
||||||
): Observable<RequestCacheEntry> {
|
|
||||||
if (!this.has(key)) {
|
|
||||||
this.store.dispatch(new RequestCacheFindByIDAction(key, service, resourceID));
|
|
||||||
}
|
|
||||||
return this.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an observable of the request with the specified key
|
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* the key of the request to get
|
|
||||||
* @return Observable<RequestCacheEntry>
|
|
||||||
* an observable of the RequestCacheEntry with the specified key
|
|
||||||
*/
|
|
||||||
get(key: string): Observable<RequestCacheEntry> {
|
|
||||||
return this.store.select<RequestCacheEntry>('core', 'cache', 'request', key)
|
|
||||||
.filter(entry => this.isValid(entry))
|
|
||||||
.distinctUntilChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether the request with the specified key is cached
|
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* the key of the request to check
|
|
||||||
* @return boolean
|
|
||||||
* true if the request with the specified key is cached,
|
|
||||||
* false otherwise
|
|
||||||
*/
|
|
||||||
has(key: string): boolean {
|
|
||||||
let result: boolean;
|
|
||||||
|
|
||||||
this.store.select<RequestCacheEntry>('core', 'cache', 'request', key)
|
|
||||||
.take(1)
|
|
||||||
.subscribe(entry => result = this.isValid(entry));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether a RequestCacheEntry should still be cached
|
|
||||||
*
|
|
||||||
* @param entry
|
|
||||||
* the entry to check
|
|
||||||
* @return boolean
|
|
||||||
* false if the entry is null, undefined, or its time to
|
|
||||||
* live has been exceeded, true otherwise
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
69
src/app/core/cache/response-cache.actions.ts
vendored
Normal file
69
src/app/core/cache/response-cache.actions.ts
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Action } from "@ngrx/store";
|
||||||
|
import { type } from "../../shared/ngrx/type";
|
||||||
|
import { Response } from "./response-cache.models";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of ResponseCacheAction type definitions
|
||||||
|
*/
|
||||||
|
export const ResponseCacheActionTypes = {
|
||||||
|
ADD: type('dspace/core/cache/request/ADD'),
|
||||||
|
REMOVE: type('dspace/core/cache/request/REMOVE'),
|
||||||
|
RESET_TIMESTAMPS: type('dspace/core/cache/request/RESET_TIMESTAMPS')
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ResponseCacheAddAction implements Action {
|
||||||
|
type = ResponseCacheActionTypes.ADD;
|
||||||
|
payload: {
|
||||||
|
key: string,
|
||||||
|
response: Response
|
||||||
|
timeAdded: number;
|
||||||
|
msToLive: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(key: string, response: Response, timeAdded: number, msToLive: number) {
|
||||||
|
this.payload = { key, response, timeAdded, msToLive };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to remove a request from the cache
|
||||||
|
*/
|
||||||
|
export class ResponseCacheRemoveAction implements Action {
|
||||||
|
type = ResponseCacheActionTypes.REMOVE;
|
||||||
|
payload: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new ResponseCacheRemoveAction
|
||||||
|
* @param key
|
||||||
|
* The key of the request to remove
|
||||||
|
*/
|
||||||
|
constructor(key: string) {
|
||||||
|
this.payload = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to reset the timeAdded property of all cached objects
|
||||||
|
*/
|
||||||
|
export class ResetResponseCacheTimestampsAction implements Action {
|
||||||
|
type = ResponseCacheActionTypes.RESET_TIMESTAMPS;
|
||||||
|
payload: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new ResetObjectCacheTimestampsAction
|
||||||
|
*
|
||||||
|
* @param newTimestamp
|
||||||
|
* the new timeAdded all objects should get
|
||||||
|
*/
|
||||||
|
constructor(newTimestamp: number) {
|
||||||
|
this.payload = newTimestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type to encompass all ResponseCacheActions
|
||||||
|
*/
|
||||||
|
export type ResponseCacheAction
|
||||||
|
= ResponseCacheAddAction
|
||||||
|
| ResponseCacheRemoveAction
|
||||||
|
| ResetResponseCacheTimestampsAction;
|
16
src/app/core/cache/response-cache.models.ts
vendored
Normal file
16
src/app/core/cache/response-cache.models.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export class Response {
|
||||||
|
constructor(public isSuccessful: boolean) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SuccessResponse extends Response {
|
||||||
|
constructor(public resourceUUIDs: Array<String>) {
|
||||||
|
super(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorResponse extends Response {
|
||||||
|
constructor(public errorMessage: string) {
|
||||||
|
super(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
225
src/app/core/cache/response-cache.reducer.spec.ts
vendored
Normal file
225
src/app/core/cache/response-cache.reducer.spec.ts
vendored
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import { responseCacheReducer, ResponseCacheState } from "./response-cache.reducer";
|
||||||
|
import {
|
||||||
|
ResponseCacheRemoveAction,
|
||||||
|
ResetResponseCacheTimestampsAction
|
||||||
|
} from "./response-cache.actions";
|
||||||
|
import deepFreeze = require("deep-freeze");
|
||||||
|
|
||||||
|
class NullAction extends ResponseCacheRemoveAction {
|
||||||
|
type = null;
|
||||||
|
payload = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// describe("responseCacheReducer", () => {
|
||||||
|
// const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"];
|
||||||
|
// const services = [new OpaqueToken('service1'), new OpaqueToken('service2')];
|
||||||
|
// const msToLive = 900000;
|
||||||
|
// const uuids = [
|
||||||
|
// "9e32a2e2-6b91-4236-a361-995ccdc14c60",
|
||||||
|
// "598ce822-c357-46f3-ab70-63724d02d6ad",
|
||||||
|
// "be8325f7-243b-49f4-8a4b-df2b793ff3b5"
|
||||||
|
// ];
|
||||||
|
// const resourceID = "9978";
|
||||||
|
// const paginationOptions = { "resultsPerPage": 10, "currentPage": 1 };
|
||||||
|
// const sortOptions = { "field": "id", "direction": 0 };
|
||||||
|
// const testState = {
|
||||||
|
// [keys[0]]: {
|
||||||
|
// "key": keys[0],
|
||||||
|
// "service": services[0],
|
||||||
|
// "resourceUUIDs": [uuids[0], uuids[1]],
|
||||||
|
// "isLoading": false,
|
||||||
|
// "paginationOptions": paginationOptions,
|
||||||
|
// "sortOptions": sortOptions,
|
||||||
|
// "timeAdded": new Date().getTime(),
|
||||||
|
// "msToLive": msToLive
|
||||||
|
// },
|
||||||
|
// [keys[1]]: {
|
||||||
|
// "key": keys[1],
|
||||||
|
// "service": services[1],
|
||||||
|
// "resourceID": resourceID,
|
||||||
|
// "resourceUUIDs": [uuids[2]],
|
||||||
|
// "isLoading": false,
|
||||||
|
// "timeAdded": new Date().getTime(),
|
||||||
|
// "msToLive": msToLive
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// deepFreeze(testState);
|
||||||
|
// const errorState: {} = {
|
||||||
|
// [keys[0]]: {
|
||||||
|
// errorMessage: 'error',
|
||||||
|
// resourceUUIDs: uuids
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// deepFreeze(errorState);
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// it("should return the current state when no valid actions have been made", () => {
|
||||||
|
// const action = new NullAction();
|
||||||
|
// const newState = responseCacheReducer(testState, action);
|
||||||
|
//
|
||||||
|
// expect(newState).toEqual(testState);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should start with an empty cache", () => {
|
||||||
|
// const action = new NullAction();
|
||||||
|
// const initialState = responseCacheReducer(undefined, action);
|
||||||
|
//
|
||||||
|
// expect(initialState).toEqual(Object.create(null));
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// describe("FIND_BY_ID", () => {
|
||||||
|
// const action = new ResponseCacheFindByIDAction(keys[0], services[0], resourceID);
|
||||||
|
//
|
||||||
|
// it("should perform the action without affecting the previous state", () => {
|
||||||
|
// //testState has already been frozen above
|
||||||
|
// responseCacheReducer(testState, action);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should add the request to the cache", () => {
|
||||||
|
// const state = Object.create(null);
|
||||||
|
// const newState = responseCacheReducer(state, action);
|
||||||
|
// expect(newState[keys[0]].key).toBe(keys[0]);
|
||||||
|
// expect(newState[keys[0]].service).toEqual(services[0]);
|
||||||
|
// expect(newState[keys[0]].resourceID).toBe(resourceID);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should set responsePending to true", () => {
|
||||||
|
// const state = Object.create(null);
|
||||||
|
// const newState = responseCacheReducer(state, action);
|
||||||
|
// expect(newState[keys[0]].responsePending).toBe(true);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should remove any previous error message or resourceUUID for the request", () => {
|
||||||
|
// const newState = responseCacheReducer(errorState, action);
|
||||||
|
// expect(newState[keys[0]].resourceUUIDs.length).toBe(0);
|
||||||
|
// expect(newState[keys[0]].errorMessage).toBeUndefined();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// describe("FIND_ALL", () => {
|
||||||
|
// const action = new ResponseCacheFindAllAction(keys[0], services[0], resourceID, paginationOptions, sortOptions);
|
||||||
|
//
|
||||||
|
// it("should perform the action without affecting the previous state", () => {
|
||||||
|
// //testState has already been frozen above
|
||||||
|
// responseCacheReducer(testState, action);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should add the request to the cache", () => {
|
||||||
|
// const state = Object.create(null);
|
||||||
|
// const newState = responseCacheReducer(state, action);
|
||||||
|
// expect(newState[keys[0]].key).toBe(keys[0]);
|
||||||
|
// expect(newState[keys[0]].service).toEqual(services[0]);
|
||||||
|
// expect(newState[keys[0]].scopeID).toBe(resourceID);
|
||||||
|
// expect(newState[keys[0]].paginationOptions).toEqual(paginationOptions);
|
||||||
|
// expect(newState[keys[0]].sortOptions).toEqual(sortOptions);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should set responsePending to true", () => {
|
||||||
|
// const state = Object.create(null);
|
||||||
|
// const newState = responseCacheReducer(state, action);
|
||||||
|
// expect(newState[keys[0]].responsePending).toBe(true);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should remove any previous error message or resourceUUIDs for the request", () => {
|
||||||
|
// const newState = responseCacheReducer(errorState, action);
|
||||||
|
// expect(newState[keys[0]].resourceUUIDs.length).toBe(0);
|
||||||
|
// expect(newState[keys[0]].errorMessage).toBeUndefined();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// describe("SUCCESS", () => {
|
||||||
|
// const successUUIDs = [uuids[0], uuids[2]];
|
||||||
|
// const successTimeAdded = new Date().getTime();
|
||||||
|
// const successMsToLive = 5;
|
||||||
|
// const action = new ResponseCacheSuccessAction(keys[0], successUUIDs, successTimeAdded, successMsToLive);
|
||||||
|
//
|
||||||
|
// it("should perform the action without affecting the previous state", () => {
|
||||||
|
// //testState has already been frozen above
|
||||||
|
// responseCacheReducer(testState, action);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should add the response to the cached request", () => {
|
||||||
|
// const newState = responseCacheReducer(testState, action);
|
||||||
|
// expect(newState[keys[0]].resourceUUIDs).toBe(successUUIDs);
|
||||||
|
// expect(newState[keys[0]].timeAdded).toBe(successTimeAdded);
|
||||||
|
// expect(newState[keys[0]].msToLive).toBe(successMsToLive);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should set responsePending to false", () => {
|
||||||
|
// const newState = responseCacheReducer(testState, action);
|
||||||
|
// expect(newState[keys[0]].responsePending).toBe(false);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should remove any previous error message for the request", () => {
|
||||||
|
// const newState = responseCacheReducer(errorState, action);
|
||||||
|
// expect(newState[keys[0]].errorMessage).toBeUndefined();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// describe("ERROR", () => {
|
||||||
|
// const errorMsg = 'errorMsg';
|
||||||
|
// const action = new ResponseCacheErrorAction(keys[0], errorMsg);
|
||||||
|
//
|
||||||
|
// it("should perform the action without affecting the previous state", () => {
|
||||||
|
// //testState has already been frozen above
|
||||||
|
// responseCacheReducer(testState, action);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should set an error message for the request", () => {
|
||||||
|
// const newState = responseCacheReducer(errorState, action);
|
||||||
|
// expect(newState[keys[0]].errorMessage).toBe(errorMsg);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should set responsePending to false", () => {
|
||||||
|
// const newState = responseCacheReducer(testState, action);
|
||||||
|
// expect(newState[keys[0]].responsePending).toBe(false);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// describe("REMOVE", () => {
|
||||||
|
// it("should perform the action without affecting the previous state", () => {
|
||||||
|
// const action = new ResponseCacheRemoveAction(keys[0]);
|
||||||
|
// //testState has already been frozen above
|
||||||
|
// responseCacheReducer(testState, action);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should remove the specified request from the cache", () => {
|
||||||
|
// const action = new ResponseCacheRemoveAction(keys[0]);
|
||||||
|
// const newState = responseCacheReducer(testState, action);
|
||||||
|
// expect(testState[keys[0]]).not.toBeUndefined();
|
||||||
|
// expect(newState[keys[0]]).toBeUndefined();
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("shouldn't do anything when the specified key isn't cached", () => {
|
||||||
|
// const wrongKey = "this isn't cached";
|
||||||
|
// const action = new ResponseCacheRemoveAction(wrongKey);
|
||||||
|
// const newState = responseCacheReducer(testState, action);
|
||||||
|
// expect(testState[wrongKey]).toBeUndefined();
|
||||||
|
// expect(newState).toEqual(testState);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// describe("RESET_TIMESTAMPS", () => {
|
||||||
|
// const newTimeStamp = new Date().getTime();
|
||||||
|
// const action = new ResetResponseCacheTimestampsAction(newTimeStamp);
|
||||||
|
//
|
||||||
|
// it("should perform the action without affecting the previous state", () => {
|
||||||
|
// //testState has already been frozen above
|
||||||
|
// responseCacheReducer(testState, action);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should set the timestamp of all requests in the cache", () => {
|
||||||
|
// const newState = responseCacheReducer(testState, action);
|
||||||
|
// Object.keys(newState).forEach((key) => {
|
||||||
|
// expect(newState[key].timeAdded).toEqual(newTimeStamp);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// });
|
112
src/app/core/cache/response-cache.reducer.ts
vendored
Normal file
112
src/app/core/cache/response-cache.reducer.ts
vendored
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import {
|
||||||
|
ResponseCacheAction, ResponseCacheActionTypes,
|
||||||
|
ResponseCacheRemoveAction, ResetResponseCacheTimestampsAction,
|
||||||
|
ResponseCacheAddAction
|
||||||
|
} from "./response-cache.actions";
|
||||||
|
import { CacheEntry } from "./cache-entry";
|
||||||
|
import { hasValue } from "../../shared/empty.util";
|
||||||
|
import { Response } from "./response-cache.models";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An entry in the ResponseCache
|
||||||
|
*/
|
||||||
|
export class ResponseCacheEntry implements CacheEntry {
|
||||||
|
key: string;
|
||||||
|
response: Response;
|
||||||
|
timeAdded: number;
|
||||||
|
msToLive: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ResponseCache State
|
||||||
|
*/
|
||||||
|
export interface ResponseCacheState {
|
||||||
|
[key: string]: ResponseCacheEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
||||||
|
const initialState = Object.create(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ResponseCache Reducer
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* the action to perform on the state
|
||||||
|
* @return ResponseCacheState
|
||||||
|
* the new state
|
||||||
|
*/
|
||||||
|
export const responseCacheReducer = (state = initialState, action: ResponseCacheAction): ResponseCacheState => {
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case ResponseCacheActionTypes.ADD: {
|
||||||
|
return addToCache(state, <ResponseCacheAddAction> action);
|
||||||
|
}
|
||||||
|
|
||||||
|
case ResponseCacheActionTypes.REMOVE: {
|
||||||
|
return removeFromCache(state, <ResponseCacheRemoveAction> action);
|
||||||
|
}
|
||||||
|
|
||||||
|
case ResponseCacheActionTypes.RESET_TIMESTAMPS: {
|
||||||
|
return resetResponseCacheTimestamps(state, <ResetResponseCacheTimestampsAction>action)
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function addToCache(state: ResponseCacheState, action: ResponseCacheAddAction): ResponseCacheState {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.payload.key]: {
|
||||||
|
key: action.payload.key,
|
||||||
|
response: action.payload.response,
|
||||||
|
timeAdded: action.payload.timeAdded,
|
||||||
|
msToLive: action.payload.msToLive
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a request from the cache
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* an ResponseCacheRemoveAction
|
||||||
|
* @return ResponseCacheState
|
||||||
|
* the new state, with the request removed if it existed.
|
||||||
|
*/
|
||||||
|
function removeFromCache(state: ResponseCacheState, action: ResponseCacheRemoveAction): ResponseCacheState {
|
||||||
|
if (hasValue(state[action.payload])) {
|
||||||
|
let newCache = Object.assign({}, state);
|
||||||
|
delete newCache[action.payload];
|
||||||
|
|
||||||
|
return newCache;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the timeAdded timestamp of every cached request to the specified value
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* a ResetResponseCacheTimestampsAction
|
||||||
|
* @return ResponseCacheState
|
||||||
|
* the new state, with all timeAdded timestamps set to the specified value
|
||||||
|
*/
|
||||||
|
function resetResponseCacheTimestamps(state: ResponseCacheState, action: ResetResponseCacheTimestampsAction): ResponseCacheState {
|
||||||
|
let newState = Object.create(null);
|
||||||
|
Object.keys(state).forEach(key => {
|
||||||
|
newState[key] = Object.assign({}, state[key], {
|
||||||
|
timeAdded: action.payload
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return newState;
|
||||||
|
}
|
146
src/app/core/cache/response-cache.service.spec.ts
vendored
Normal file
146
src/app/core/cache/response-cache.service.spec.ts
vendored
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { ResponseCacheService } from "./response-cache.service";
|
||||||
|
import { Store } from "@ngrx/store";
|
||||||
|
import { ResponseCacheState, ResponseCacheEntry } from "./response-cache.reducer";
|
||||||
|
import { OpaqueToken } from "@angular/core";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
// describe("ResponseCacheService", () => {
|
||||||
|
// let service: ResponseCacheService;
|
||||||
|
// let store: Store<ResponseCacheState>;
|
||||||
|
//
|
||||||
|
// const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"];
|
||||||
|
// const serviceTokens = [new OpaqueToken('service1'), new OpaqueToken('service2')];
|
||||||
|
// const resourceID = "9978";
|
||||||
|
// const paginationOptions = { "resultsPerPage": 10, "currentPage": 1 };
|
||||||
|
// const sortOptions = { "field": "id", "direction": 0 };
|
||||||
|
// const timestamp = new Date().getTime();
|
||||||
|
// const validCacheEntry = (key) => {
|
||||||
|
// return {
|
||||||
|
// key: key,
|
||||||
|
// timeAdded: timestamp,
|
||||||
|
// msToLive: 24 * 60 * 60 * 1000 // a day
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// const invalidCacheEntry = (key) => {
|
||||||
|
// return {
|
||||||
|
// key: key,
|
||||||
|
// timeAdded: 0,
|
||||||
|
// msToLive: 0
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// beforeEach(() => {
|
||||||
|
// store = new Store<ResponseCacheState>(undefined, undefined, undefined);
|
||||||
|
// spyOn(store, 'dispatch');
|
||||||
|
// service = new ResponseCacheService(store);
|
||||||
|
// spyOn(window, 'Date').and.returnValue({ getTime: () => timestamp });
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// describe("findAll", () => {
|
||||||
|
// beforeEach(() => {
|
||||||
|
// spyOn(service, "get").and.callFake((key) => Observable.of({key: key}));
|
||||||
|
// });
|
||||||
|
// describe("if the key isn't cached", () => {
|
||||||
|
// beforeEach(() => {
|
||||||
|
// spyOn(service, "has").and.returnValue(false);
|
||||||
|
// });
|
||||||
|
// it("should dispatch a FIND_ALL action with the key, service, scopeID, paginationOptions and sortOptions", () => {
|
||||||
|
// service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions);
|
||||||
|
// expect(store.dispatch).toHaveBeenCalledWith(new ResponseCacheFindAllAction(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions))
|
||||||
|
// });
|
||||||
|
// it("should return an observable of the newly cached request with the specified key", () => {
|
||||||
|
// let result: ResponseCacheEntry;
|
||||||
|
// service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry);
|
||||||
|
// expect(result.key).toEqual(keys[0]);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// describe("if the key is already cached", () => {
|
||||||
|
// beforeEach(() => {
|
||||||
|
// spyOn(service, "has").and.returnValue(true);
|
||||||
|
// });
|
||||||
|
// it("shouldn't dispatch anything", () => {
|
||||||
|
// service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions);
|
||||||
|
// expect(store.dispatch).not.toHaveBeenCalled();
|
||||||
|
// });
|
||||||
|
// it("should return an observable of the existing cached request with the specified key", () => {
|
||||||
|
// let result: ResponseCacheEntry;
|
||||||
|
// service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry);
|
||||||
|
// expect(result.key).toEqual(keys[0]);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// describe("findById", () => {
|
||||||
|
// beforeEach(() => {
|
||||||
|
// spyOn(service, "get").and.callFake((key) => Observable.of({key: key}));
|
||||||
|
// });
|
||||||
|
// describe("if the key isn't cached", () => {
|
||||||
|
// beforeEach(() => {
|
||||||
|
// spyOn(service, "has").and.returnValue(false);
|
||||||
|
// });
|
||||||
|
// it("should dispatch a FIND_BY_ID action with the key, service, and resourceID", () => {
|
||||||
|
// service.findById(keys[0], serviceTokens[0], resourceID);
|
||||||
|
// expect(store.dispatch).toHaveBeenCalledWith(new ResponseCacheFindByIDAction(keys[0], serviceTokens[0], resourceID))
|
||||||
|
// });
|
||||||
|
// it("should return an observable of the newly cached request with the specified key", () => {
|
||||||
|
// let result: ResponseCacheEntry;
|
||||||
|
// service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry);
|
||||||
|
// expect(result.key).toEqual(keys[0]);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// describe("if the key is already cached", () => {
|
||||||
|
// beforeEach(() => {
|
||||||
|
// spyOn(service, "has").and.returnValue(true);
|
||||||
|
// });
|
||||||
|
// it("shouldn't dispatch anything", () => {
|
||||||
|
// service.findById(keys[0], serviceTokens[0], resourceID);
|
||||||
|
// expect(store.dispatch).not.toHaveBeenCalled();
|
||||||
|
// });
|
||||||
|
// it("should return an observable of the existing cached request with the specified key", () => {
|
||||||
|
// let result: ResponseCacheEntry;
|
||||||
|
// service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry);
|
||||||
|
// expect(result.key).toEqual(keys[0]);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// describe("get", () => {
|
||||||
|
// it("should return an observable of the cached request with the specified key", () => {
|
||||||
|
// spyOn(store, "select").and.callFake((...args:Array<any>) => {
|
||||||
|
// return Observable.of(validCacheEntry(args[args.length - 1]));
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// let testObj: ResponseCacheEntry;
|
||||||
|
// service.get(keys[1]).take(1).subscribe(entry => testObj = entry);
|
||||||
|
// expect(testObj.key).toEqual(keys[1]);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should not return a cached request that has exceeded its time to live", () => {
|
||||||
|
// spyOn(store, "select").and.callFake((...args:Array<any>) => {
|
||||||
|
// return Observable.of(invalidCacheEntry(args[args.length - 1]));
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// let getObsHasFired = false;
|
||||||
|
// const subscription = service.get(keys[1]).subscribe(entry => getObsHasFired = true);
|
||||||
|
// expect(getObsHasFired).toBe(false);
|
||||||
|
// subscription.unsubscribe();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// describe("has", () => {
|
||||||
|
// it("should return true if the request with the supplied key is cached and still valid", () => {
|
||||||
|
// spyOn(store, 'select').and.returnValue(Observable.of(validCacheEntry(keys[1])));
|
||||||
|
// expect(service.has(keys[1])).toBe(true);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should return false if the request with the supplied key isn't cached", () => {
|
||||||
|
// spyOn(store, 'select').and.returnValue(Observable.of(undefined));
|
||||||
|
// expect(service.has(keys[1])).toBe(false);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it("should return false if the request with the supplied key is cached but has exceeded its time to live", () => {
|
||||||
|
// spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry(keys[1])));
|
||||||
|
// expect(service.has(keys[1])).toBe(false);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
87
src/app/core/cache/response-cache.service.ts
vendored
Normal file
87
src/app/core/cache/response-cache.service.ts
vendored
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { Store } from "@ngrx/store";
|
||||||
|
import {
|
||||||
|
ResponseCacheState, ResponseCacheEntry
|
||||||
|
} from "./response-cache.reducer";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
import { hasNoValue } from "../../shared/empty.util";
|
||||||
|
import {
|
||||||
|
ResponseCacheRemoveAction,
|
||||||
|
ResponseCacheAddAction
|
||||||
|
} from "./response-cache.actions";
|
||||||
|
import { Response } from "./response-cache.models";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service to interact with the response cache
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ResponseCacheService {
|
||||||
|
constructor(
|
||||||
|
private store: Store<ResponseCacheState>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
add(key: string, response: Response, msToLive: number): Observable<ResponseCacheEntry> {
|
||||||
|
if (!this.has(key)) {
|
||||||
|
// this.store.dispatch(new ResponseCacheFindAllAction(key, service, scopeID, paginationOptions, sortOptions));
|
||||||
|
this.store.dispatch(new ResponseCacheAddAction(key, response, new Date().getTime(), msToLive));
|
||||||
|
}
|
||||||
|
return this.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an observable of the response with the specified key
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* the key of the response to get
|
||||||
|
* @return Observable<ResponseCacheEntry>
|
||||||
|
* an observable of the ResponseCacheEntry with the specified key
|
||||||
|
*/
|
||||||
|
get(key: string): Observable<ResponseCacheEntry> {
|
||||||
|
return this.store.select<ResponseCacheEntry>('core', 'cache', 'response', key)
|
||||||
|
.filter(entry => this.isValid(entry))
|
||||||
|
.distinctUntilChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the response with the specified key is cached
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* the key of the response to check
|
||||||
|
* @return boolean
|
||||||
|
* true if the response with the specified key is cached,
|
||||||
|
* false otherwise
|
||||||
|
*/
|
||||||
|
has(key: string): boolean {
|
||||||
|
let result: boolean;
|
||||||
|
|
||||||
|
this.store.select<ResponseCacheEntry>('core', 'cache', 'response', key)
|
||||||
|
.take(1)
|
||||||
|
.subscribe(entry => result = this.isValid(entry));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a ResponseCacheEntry should still be cached
|
||||||
|
*
|
||||||
|
* @param entry
|
||||||
|
* the entry to check
|
||||||
|
* @return boolean
|
||||||
|
* false if the entry is null, undefined, or its time to
|
||||||
|
* live has been exceeded, true otherwise
|
||||||
|
*/
|
||||||
|
private isValid(entry: ResponseCacheEntry): 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 ResponseCacheRemoveAction(entry.key));
|
||||||
|
}
|
||||||
|
return !isOutDated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,12 +1,11 @@
|
|||||||
import { EffectsModule } from "@ngrx/effects";
|
import { EffectsModule } from "@ngrx/effects";
|
||||||
import { CollectionDataEffects } from "./data-services/collection-data.effects";
|
import { ObjectCacheEffects } from "./data/object-cache.effects";
|
||||||
import { ItemDataEffects } from "./data-services/item-data.effects";
|
import { RequestCacheEffects } from "./data/request-cache.effects";
|
||||||
import { ObjectCacheEffects } from "./data-services/object-cache.effects";
|
import { HrefIndexEffects } from "./index/href-index.effects";
|
||||||
import { RequestCacheEffects } from "./data-services/request-cache.effects";
|
import { RequestEffects } from "./data/request.effects";
|
||||||
|
|
||||||
export const coreEffects = [
|
export const coreEffects = [
|
||||||
EffectsModule.run(CollectionDataEffects),
|
EffectsModule.run(RequestEffects),
|
||||||
EffectsModule.run(ItemDataEffects),
|
|
||||||
EffectsModule.run(RequestCacheEffects),
|
|
||||||
EffectsModule.run(ObjectCacheEffects),
|
EffectsModule.run(ObjectCacheEffects),
|
||||||
|
EffectsModule.run(HrefIndexEffects),
|
||||||
];
|
];
|
||||||
|
@@ -5,9 +5,9 @@ import { isNotEmpty } from "../shared/empty.util";
|
|||||||
import { FooterComponent } from "./footer/footer.component";
|
import { FooterComponent } from "./footer/footer.component";
|
||||||
import { DSpaceRESTv2Service } from "./dspace-rest-v2/dspace-rest-v2.service";
|
import { DSpaceRESTv2Service } from "./dspace-rest-v2/dspace-rest-v2.service";
|
||||||
import { ObjectCacheService } from "./cache/object-cache.service";
|
import { ObjectCacheService } from "./cache/object-cache.service";
|
||||||
import { RequestCacheService } from "./cache/request-cache.service";
|
import { ResponseCacheService } from "./cache/response-cache.service";
|
||||||
import { CollectionDataService } from "./data-services/collection-data.service";
|
import { CollectionDataService } from "./data/collection-data.service";
|
||||||
import { ItemDataService } from "./data-services/item-data.service";
|
import { ItemDataService } from "./data/item-data.service";
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -27,7 +27,7 @@ const PROVIDERS = [
|
|||||||
ItemDataService,
|
ItemDataService,
|
||||||
DSpaceRESTv2Service,
|
DSpaceRESTv2Service,
|
||||||
ObjectCacheService,
|
ObjectCacheService,
|
||||||
RequestCacheService
|
ResponseCacheService
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -1,12 +1,18 @@
|
|||||||
import { combineReducers } from "@ngrx/store";
|
import { combineReducers } from "@ngrx/store";
|
||||||
import { CacheState, cacheReducer } from "./cache/cache.reducers";
|
import { CacheState, cacheReducer } from "./cache/cache.reducers";
|
||||||
|
import { IndexState, indexReducer } from "./index/index.reducers";
|
||||||
|
import { DataState, dataReducer } from "./data/data.reducers";
|
||||||
|
|
||||||
export interface CoreState {
|
export interface CoreState {
|
||||||
cache: CacheState
|
cache: CacheState,
|
||||||
|
index: IndexState,
|
||||||
|
data: DataState
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reducers = {
|
export const reducers = {
|
||||||
cache: cacheReducer
|
cache: cacheReducer,
|
||||||
|
index: indexReducer,
|
||||||
|
data: dataReducer
|
||||||
};
|
};
|
||||||
|
|
||||||
export function coreReducer(state: any, action: any) {
|
export function coreReducer(state: any, action: any) {
|
||||||
|
@@ -1,41 +0,0 @@
|
|||||||
import { Inject, Injectable } from "@angular/core";
|
|
||||||
import { DataEffects } from "./data.effects";
|
|
||||||
import { Serializer } from "../serializer";
|
|
||||||
import { Collection } from "../shared/collection.model";
|
|
||||||
import { DSpaceRESTv2Serializer } from "../dspace-rest-v2/dspace-rest-v2.serializer";
|
|
||||||
import { ObjectCacheService } from "../cache/object-cache.service";
|
|
||||||
import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
|
|
||||||
import { Actions, Effect } from "@ngrx/effects";
|
|
||||||
import { RequestCacheFindAllAction, RequestCacheFindByIDAction } from "../cache/request-cache.actions";
|
|
||||||
import { CollectionDataService } from "./collection-data.service";
|
|
||||||
|
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CollectionDataEffects extends DataEffects<Collection> {
|
|
||||||
constructor(
|
|
||||||
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig,
|
|
||||||
actions$: Actions,
|
|
||||||
restApi: DSpaceRESTv2Service,
|
|
||||||
cache: ObjectCacheService,
|
|
||||||
dataService: CollectionDataService
|
|
||||||
) {
|
|
||||||
super(EnvConfig, actions$, restApi, cache, dataService);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getFindAllEndpoint(action: RequestCacheFindAllAction): string {
|
|
||||||
return '/collections';
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getFindByIdEndpoint(action: RequestCacheFindByIDAction): string {
|
|
||||||
return `/collections/${action.payload.resourceID}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getSerializer(): Serializer<Collection> {
|
|
||||||
return new DSpaceRESTv2Serializer(Collection);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Effect() findAll$ = this.findAll;
|
|
||||||
|
|
||||||
@Effect() findById$ = this.findById;
|
|
||||||
}
|
|
@@ -1,18 +0,0 @@
|
|||||||
import { Injectable, OpaqueToken } from "@angular/core";
|
|
||||||
import { DataService } from "./data.service";
|
|
||||||
import { Collection } from "../shared/collection.model";
|
|
||||||
import { ObjectCacheService } from "../cache/object-cache.service";
|
|
||||||
import { RequestCacheService } from "../cache/request-cache.service";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CollectionDataService extends DataService<Collection> {
|
|
||||||
serviceName = new OpaqueToken('CollectionDataService');
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected objectCache: ObjectCacheService,
|
|
||||||
protected requestCache: RequestCacheService,
|
|
||||||
) {
|
|
||||||
super(Collection);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,68 +0,0 @@
|
|||||||
import { Inject } from "@angular/core";
|
|
||||||
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";
|
|
||||||
import { ObjectCacheService } from "../cache/object-cache.service";
|
|
||||||
import { CacheableObject } from "../cache/object-cache.reducer";
|
|
||||||
import { Serializer } from "../serializer";
|
|
||||||
import {
|
|
||||||
RequestCacheActionTypes, RequestCacheFindAllAction, RequestCacheSuccessAction,
|
|
||||||
RequestCacheErrorAction, RequestCacheFindByIDAction
|
|
||||||
} from "../cache/request-cache.actions";
|
|
||||||
import { DataService } from "./data.service";
|
|
||||||
import { hasNoValue } from "../../shared/empty.util";
|
|
||||||
|
|
||||||
import { GlobalConfig } from '../../../config';
|
|
||||||
|
|
||||||
export abstract class DataEffects<T extends CacheableObject> {
|
|
||||||
protected abstract getFindAllEndpoint(action: RequestCacheFindAllAction): string;
|
|
||||||
protected abstract getFindByIdEndpoint(action: RequestCacheFindByIDAction): string;
|
|
||||||
protected abstract getSerializer(): Serializer<T>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private EnvConfig: GlobalConfig,
|
|
||||||
private actions$: Actions,
|
|
||||||
private restApi: DSpaceRESTv2Service,
|
|
||||||
private objectCache: ObjectCacheService,
|
|
||||||
private dataService: DataService<T>
|
|
||||||
) { }
|
|
||||||
|
|
||||||
// TODO, results of a findall aren't retrieved from cache yet
|
|
||||||
protected findAll = this.actions$
|
|
||||||
.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))
|
|
||||||
.do((ts: T[]) => {
|
|
||||||
ts.forEach((t) => {
|
|
||||||
if (hasNoValue(t) || hasNoValue(t.uuid)) {
|
|
||||||
throw new Error('The server returned an invalid object');
|
|
||||||
}
|
|
||||||
this.objectCache.add(t, this.EnvConfig.cache.msToLive);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.map((ts: Array<T>) => ts.map(t => t.uuid))
|
|
||||||
.map((ids: Array<string>) => new RequestCacheSuccessAction(action.payload.key, ids, new Date().getTime(), this.EnvConfig.cache.msToLive))
|
|
||||||
.catch((error: Error) => Observable.of(new RequestCacheErrorAction(action.payload.key, error.message)));
|
|
||||||
});
|
|
||||||
|
|
||||||
protected findById = this.actions$
|
|
||||||
.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) => {
|
|
||||||
if (hasNoValue(t) || hasNoValue(t.uuid)) {
|
|
||||||
throw new Error('The server returned an invalid object');
|
|
||||||
}
|
|
||||||
this.objectCache.add(t, this.EnvConfig.cache.msToLive);
|
|
||||||
})
|
|
||||||
.map((t: T) => new RequestCacheSuccessAction(action.payload.key, [t.uuid], new Date().getTime(), this.EnvConfig.cache.msToLive))
|
|
||||||
.catch((error: Error) => Observable.of(new RequestCacheErrorAction(action.payload.key, error.message)));
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
@@ -1,54 +0,0 @@
|
|||||||
import { OpaqueToken } from "@angular/core";
|
|
||||||
import { Observable } from "rxjs";
|
|
||||||
import { ObjectCacheService } from "../cache/object-cache.service";
|
|
||||||
import { RequestCacheService } from "../cache/request-cache.service";
|
|
||||||
import { CacheableObject } from "../cache/object-cache.reducer";
|
|
||||||
import { ParamHash } from "../shared/param-hash";
|
|
||||||
import { isNotEmpty } from "../../shared/empty.util";
|
|
||||||
import { GenericConstructor } from "../shared/generic-constructor";
|
|
||||||
import { RemoteData } from "./remote-data";
|
|
||||||
|
|
||||||
export abstract class DataService<T extends CacheableObject> {
|
|
||||||
abstract serviceName: OpaqueToken;
|
|
||||||
protected abstract objectCache: ObjectCacheService;
|
|
||||||
protected abstract requestCache: RequestCacheService;
|
|
||||||
|
|
||||||
constructor(private modelType: GenericConstructor<T>) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
findAll(scopeID?: string): RemoteData<Array<T>> {
|
|
||||||
const key = new ParamHash(this.serviceName, 'findAll', scopeID).toString();
|
|
||||||
const requestCacheObs = this.requestCache.findAll(key, this.serviceName, scopeID);
|
|
||||||
return new RemoteData(
|
|
||||||
requestCacheObs.map(entry => entry.isLoading).distinctUntilChanged(),
|
|
||||||
requestCacheObs.map(entry => entry.errorMessage).distinctUntilChanged(),
|
|
||||||
requestCacheObs
|
|
||||||
.map(entry => entry.resourceUUIDs)
|
|
||||||
.flatMap((resourceUUIDs: Array<string>) => {
|
|
||||||
// use those IDs to fetch the actual objects from the ObjectCache
|
|
||||||
return this.objectCache.getList<T>(resourceUUIDs, this.modelType);
|
|
||||||
}).distinctUntilChanged()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
findById(id: string): RemoteData<T> {
|
|
||||||
const key = new ParamHash(this.serviceName, 'findById', id).toString();
|
|
||||||
const requestCacheObs = this.requestCache.findById(key, this.serviceName, id);
|
|
||||||
return new RemoteData(
|
|
||||||
requestCacheObs.map(entry => entry.isLoading).distinctUntilChanged(),
|
|
||||||
requestCacheObs.map(entry => entry.errorMessage).distinctUntilChanged(),
|
|
||||||
requestCacheObs
|
|
||||||
.map(entry => entry.resourceUUIDs)
|
|
||||||
.flatMap((resourceUUIDs: Array<string>) => {
|
|
||||||
if (isNotEmpty(resourceUUIDs)) {
|
|
||||||
return this.objectCache.get<T>(resourceUUIDs[0], this.modelType);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return Observable.of(undefined);
|
|
||||||
}
|
|
||||||
}).distinctUntilChanged()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,41 +0,0 @@
|
|||||||
import { Inject, Injectable } from "@angular/core";
|
|
||||||
import { DataEffects } from "./data.effects";
|
|
||||||
import { Serializer } from "../serializer";
|
|
||||||
import { Item } from "../shared/item.model";
|
|
||||||
import { DSpaceRESTv2Serializer } from "../dspace-rest-v2/dspace-rest-v2.serializer";
|
|
||||||
import { ObjectCacheService } from "../cache/object-cache.service";
|
|
||||||
import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
|
|
||||||
import { Actions, Effect } from "@ngrx/effects";
|
|
||||||
import { RequestCacheFindAllAction, RequestCacheFindByIDAction } from "../cache/request-cache.actions";
|
|
||||||
import { ItemDataService } from "./item-data.service";
|
|
||||||
|
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ItemDataEffects extends DataEffects<Item> {
|
|
||||||
constructor(
|
|
||||||
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig,
|
|
||||||
actions$: Actions,
|
|
||||||
restApi: DSpaceRESTv2Service,
|
|
||||||
cache: ObjectCacheService,
|
|
||||||
dataService: ItemDataService
|
|
||||||
) {
|
|
||||||
super(EnvConfig, actions$, restApi, cache, dataService);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getFindAllEndpoint(action: RequestCacheFindAllAction): string {
|
|
||||||
return '/items';
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getFindByIdEndpoint(action: RequestCacheFindByIDAction): string {
|
|
||||||
return `/items/${action.payload.resourceID}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getSerializer(): Serializer<Item> {
|
|
||||||
return new DSpaceRESTv2Serializer(Item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Effect() findAll$ = this.findAll;
|
|
||||||
|
|
||||||
@Effect() findById$ = this.findById;
|
|
||||||
}
|
|
@@ -1,18 +0,0 @@
|
|||||||
import { Injectable, OpaqueToken } from "@angular/core";
|
|
||||||
import { DataService } from "./data.service";
|
|
||||||
import { Item } from "../shared/item.model";
|
|
||||||
import { ObjectCacheService } from "../cache/object-cache.service";
|
|
||||||
import { RequestCacheService } from "../cache/request-cache.service";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ItemDataService extends DataService<Item> {
|
|
||||||
serviceName = new OpaqueToken('ItemDataService');
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected objectCache: ObjectCacheService,
|
|
||||||
protected requestCache: RequestCacheService,
|
|
||||||
) {
|
|
||||||
super(Item);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
36
src/app/core/data/collection-data.service.ts
Normal file
36
src/app/core/data/collection-data.service.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { DataService } from "./data.service";
|
||||||
|
import { Collection } from "../shared/collection.model";
|
||||||
|
import { ObjectCacheService } from "../cache/object-cache.service";
|
||||||
|
import { ResponseCacheService } from "../cache/response-cache.service";
|
||||||
|
import { RemoteData } from "./remote-data";
|
||||||
|
import { ItemDataService } from "./item-data.service";
|
||||||
|
import { Store } from "@ngrx/store";
|
||||||
|
import { RequestState } from "./request.reducer";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CollectionDataService extends DataService<Collection> {
|
||||||
|
protected endpoint = '/collections';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected responseCache: ResponseCacheService,
|
||||||
|
protected store: Store<RequestState>,
|
||||||
|
protected ids: ItemDataService
|
||||||
|
) {
|
||||||
|
super(Collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// findAll(scopeID?: string): RemoteData<Array<Collection>> {
|
||||||
|
// let remoteData = super.findAll(scopeID);
|
||||||
|
// remoteData.payload = remoteData.payload.map(collections => {
|
||||||
|
// return collections.map(collection => {
|
||||||
|
// collection.items = collection.itemLinks.map(item => {
|
||||||
|
// return this.ids.findById(item.self);
|
||||||
|
// });
|
||||||
|
// return collection
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// return remoteData;
|
||||||
|
// }
|
||||||
|
}
|
14
src/app/core/data/data.reducers.ts
Normal file
14
src/app/core/data/data.reducers.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { combineReducers } from "@ngrx/store";
|
||||||
|
import { RequestState, requestReducer } from "./request.reducer";
|
||||||
|
|
||||||
|
export interface DataState {
|
||||||
|
request: RequestState
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducers = {
|
||||||
|
request: requestReducer
|
||||||
|
};
|
||||||
|
|
||||||
|
export function dataReducer(state: any, action: any) {
|
||||||
|
return combineReducers(reducers)(state, action);
|
||||||
|
}
|
93
src/app/core/data/data.service.ts
Normal file
93
src/app/core/data/data.service.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
import { ObjectCacheService } from "../cache/object-cache.service";
|
||||||
|
import { ResponseCacheService } from "../cache/response-cache.service";
|
||||||
|
import { CacheableObject } from "../cache/object-cache.reducer";
|
||||||
|
import { isNotEmpty, hasValue } from "../../shared/empty.util";
|
||||||
|
import { GenericConstructor } from "../shared/generic-constructor";
|
||||||
|
import { RemoteData } from "./remote-data";
|
||||||
|
import { SuccessResponse, ErrorResponse } from "../cache/response-cache.models";
|
||||||
|
import { FindAllRequest, FindByIDRequest } from "./request.models";
|
||||||
|
import { RequestState, RequestEntry } from "./request.reducer";
|
||||||
|
import { Store } from "@ngrx/store";
|
||||||
|
import { RequestConfigureAction, RequestExecuteAction } from "./request.actions";
|
||||||
|
import { ResponseCacheEntry } from "../cache/response-cache.reducer";
|
||||||
|
|
||||||
|
export abstract class DataService<T extends CacheableObject> {
|
||||||
|
protected abstract objectCache: ObjectCacheService;
|
||||||
|
protected abstract responseCache: ResponseCacheService;
|
||||||
|
protected abstract store: Store<RequestState>;
|
||||||
|
protected abstract endpoint: string;
|
||||||
|
|
||||||
|
constructor(private resourceType: GenericConstructor<T>) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getFindAllHref(scopeID?): string {
|
||||||
|
let result = this.endpoint;
|
||||||
|
if (hasValue(scopeID)) {
|
||||||
|
result += `?scope=${scopeID}`
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll(scopeID?: string): RemoteData<Array<T>> {
|
||||||
|
const href = this.getFindAllHref(scopeID);
|
||||||
|
const request = new FindAllRequest(href, this.resourceType, scopeID);
|
||||||
|
this.store.dispatch(new RequestConfigureAction(request));
|
||||||
|
this.store.dispatch(new RequestExecuteAction(href));
|
||||||
|
const requestObs = this.store.select('core', 'data', 'request', href);
|
||||||
|
const responseCacheObs = this.responseCache.get(href);
|
||||||
|
return new RemoteData(
|
||||||
|
requestObs.map((entry: RequestEntry) => entry.requestPending).distinctUntilChanged(),
|
||||||
|
requestObs.map((entry: RequestEntry) => entry.responsePending).distinctUntilChanged(),
|
||||||
|
responseCacheObs
|
||||||
|
.map((entry: ResponseCacheEntry) => entry.response.isSuccessful).distinctUntilChanged(),
|
||||||
|
responseCacheObs
|
||||||
|
.filter((entry: ResponseCacheEntry) => !entry.response.isSuccessful)
|
||||||
|
.map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
|
||||||
|
.distinctUntilChanged(),
|
||||||
|
responseCacheObs
|
||||||
|
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
||||||
|
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
|
||||||
|
.flatMap((resourceUUIDs: Array<string>) => {
|
||||||
|
// use those IDs to fetch the actual objects from the ObjectCache
|
||||||
|
return this.objectCache.getList<T>(resourceUUIDs, this.resourceType);
|
||||||
|
}).distinctUntilChanged()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getFindByIDHref(resourceID): string {
|
||||||
|
return `${this.endpoint}/${resourceID}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
findById(id: string): RemoteData<T> {
|
||||||
|
const href = this.getFindByIDHref(id);
|
||||||
|
const request = new FindByIDRequest(href, this.resourceType, id);
|
||||||
|
this.store.dispatch(new RequestConfigureAction(request));
|
||||||
|
this.store.dispatch(new RequestExecuteAction(href));
|
||||||
|
const requestObs = this.store.select('core', 'data', 'request', href);
|
||||||
|
const responseCacheObs = this.responseCache.get(href);
|
||||||
|
return new RemoteData(
|
||||||
|
requestObs.map((entry: RequestEntry) => entry.requestPending).distinctUntilChanged(),
|
||||||
|
requestObs.map((entry: RequestEntry) => entry.responsePending).distinctUntilChanged(),
|
||||||
|
responseCacheObs
|
||||||
|
.map((entry: ResponseCacheEntry) => entry.response.isSuccessful).distinctUntilChanged(),
|
||||||
|
responseCacheObs
|
||||||
|
.filter((entry: ResponseCacheEntry) => !entry.response.isSuccessful)
|
||||||
|
.map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
|
||||||
|
.distinctUntilChanged(),
|
||||||
|
responseCacheObs
|
||||||
|
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
||||||
|
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
|
||||||
|
.flatMap((resourceUUIDs: Array<string>) => {
|
||||||
|
if (isNotEmpty(resourceUUIDs)) {
|
||||||
|
return this.objectCache.get<T>(resourceUUIDs[0], this.resourceType);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Observable.of(undefined);
|
||||||
|
}
|
||||||
|
}).distinctUntilChanged()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
21
src/app/core/data/item-data.service.ts
Normal file
21
src/app/core/data/item-data.service.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { DataService } from "./data.service";
|
||||||
|
import { Item } from "../shared/item.model";
|
||||||
|
import { ObjectCacheService } from "../cache/object-cache.service";
|
||||||
|
import { ResponseCacheService } from "../cache/response-cache.service";
|
||||||
|
import { Store } from "@ngrx/store";
|
||||||
|
import { RequestState } from "./request.reducer";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ItemDataService extends DataService<Item> {
|
||||||
|
protected endpoint = '/items';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected responseCache: ResponseCacheService,
|
||||||
|
protected store: Store<RequestState>
|
||||||
|
) {
|
||||||
|
super(Item);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -2,15 +2,12 @@ import { Injectable } from "@angular/core";
|
|||||||
import { Actions, Effect } from "@ngrx/effects";
|
import { Actions, Effect } from "@ngrx/effects";
|
||||||
import { StoreActionTypes } from "../../store.actions";
|
import { StoreActionTypes } from "../../store.actions";
|
||||||
import { ResetObjectCacheTimestampsAction } from "../cache/object-cache.actions";
|
import { ResetObjectCacheTimestampsAction } from "../cache/object-cache.actions";
|
||||||
import { Store } from "@ngrx/store";
|
|
||||||
import { ObjectCacheState } from "../cache/object-cache.reducer";
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ObjectCacheEffects {
|
export class ObjectCacheEffects {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private actions$: Actions,
|
private actions$: Actions
|
||||||
private store: Store<ObjectCacheState>
|
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
@@ -1,8 +1,6 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
import { hasValue } from "../../shared/empty.util";
|
|
||||||
|
|
||||||
export enum RemoteDataState {
|
export enum RemoteDataState {
|
||||||
//TODO RequestPending will never happen: implement it in the store & DataEffects.
|
|
||||||
RequestPending,
|
RequestPending,
|
||||||
ResponsePending,
|
ResponsePending,
|
||||||
Failed,
|
Failed,
|
||||||
@@ -10,12 +8,14 @@ export enum RemoteDataState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class to represent the state of
|
* A class to represent the state of a remote resource
|
||||||
*/
|
*/
|
||||||
export class RemoteData<T> {
|
export class RemoteData<T> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private storeLoading: Observable<boolean>,
|
private requestPending: Observable<boolean>,
|
||||||
|
private responsePending: Observable<boolean>,
|
||||||
|
private isSuccessFul: Observable<boolean>,
|
||||||
public errorMessage: Observable<string>,
|
public errorMessage: Observable<string>,
|
||||||
public payload: Observable<T>
|
public payload: Observable<T>
|
||||||
) {
|
) {
|
||||||
@@ -23,13 +23,17 @@ export class RemoteData<T> {
|
|||||||
|
|
||||||
get state(): Observable<RemoteDataState> {
|
get state(): Observable<RemoteDataState> {
|
||||||
return Observable.combineLatest(
|
return Observable.combineLatest(
|
||||||
this.storeLoading,
|
this.requestPending,
|
||||||
this.errorMessage.map(msg => hasValue(msg)),
|
this.responsePending,
|
||||||
(storeLoading, hasMsg) => {
|
this.isSuccessFul,
|
||||||
if (storeLoading) {
|
(requestPending, responsePending, isSuccessFul) => {
|
||||||
|
if (requestPending) {
|
||||||
|
return RemoteDataState.RequestPending
|
||||||
|
}
|
||||||
|
else if (responsePending) {
|
||||||
return RemoteDataState.ResponsePending
|
return RemoteDataState.ResponsePending
|
||||||
}
|
}
|
||||||
else if (hasMsg) {
|
else if (!isSuccessFul) {
|
||||||
return RemoteDataState.Failed
|
return RemoteDataState.Failed
|
||||||
}
|
}
|
||||||
else {
|
else {
|
@@ -1,16 +1,15 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable, Inject } from "@angular/core";
|
||||||
import { Actions, Effect } from "@ngrx/effects";
|
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";
|
import { ObjectCacheActionTypes } from "../cache/object-cache.actions";
|
||||||
|
import { GlobalConfig, GLOBAL_CONFIG } from "../../../config";
|
||||||
|
import { ResetResponseCacheTimestampsAction } from "../cache/response-cache.actions";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RequestCacheEffects {
|
export class RequestCacheEffects {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
|
||||||
private actions$: Actions,
|
private actions$: Actions,
|
||||||
private store: Store<RequestCacheState>
|
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +30,5 @@ export class RequestCacheEffects {
|
|||||||
*/
|
*/
|
||||||
@Effect() fixTimestampsOnRehydrate = this.actions$
|
@Effect() fixTimestampsOnRehydrate = this.actions$
|
||||||
.ofType(ObjectCacheActionTypes.RESET_TIMESTAMPS)
|
.ofType(ObjectCacheActionTypes.RESET_TIMESTAMPS)
|
||||||
.map(() => new ResetRequestCacheTimestampsAction(new Date().getTime()));
|
.map(() => new ResetResponseCacheTimestampsAction(new Date().getTime()));
|
||||||
|
|
||||||
}
|
}
|
61
src/app/core/data/request.actions.ts
Normal file
61
src/app/core/data/request.actions.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Action } from "@ngrx/store";
|
||||||
|
import { type } from "../../shared/ngrx/type";
|
||||||
|
import { PaginationOptions } from "../shared/pagination-options.model";
|
||||||
|
import { SortOptions } from "../shared/sort-options.model";
|
||||||
|
import { CacheableObject } from "../cache/object-cache.reducer";
|
||||||
|
import { Request } from "./request.models";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of RequestAction type definitions
|
||||||
|
*/
|
||||||
|
export const RequestActionTypes = {
|
||||||
|
CONFIGURE: type('dspace/core/data/request/CONFIGURE'),
|
||||||
|
EXECUTE: type('dspace/core/data/request/EXECUTE'),
|
||||||
|
COMPLETE: type('dspace/core/data/request/COMPLETE')
|
||||||
|
};
|
||||||
|
|
||||||
|
export class RequestConfigureAction implements Action {
|
||||||
|
type = RequestActionTypes.CONFIGURE;
|
||||||
|
payload: Request<CacheableObject>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
request: Request<CacheableObject>
|
||||||
|
) {
|
||||||
|
this.payload = request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RequestExecuteAction implements Action {
|
||||||
|
type = RequestActionTypes.EXECUTE;
|
||||||
|
payload: string;
|
||||||
|
|
||||||
|
constructor(key: string) {
|
||||||
|
this.payload = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to indicate a response was returned
|
||||||
|
*/
|
||||||
|
export class RequestCompleteAction implements Action {
|
||||||
|
type = RequestActionTypes.COMPLETE;
|
||||||
|
payload: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new RequestCompleteAction
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* the key under which this request is stored,
|
||||||
|
*/
|
||||||
|
constructor(key: string) {
|
||||||
|
this.payload = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type to encompass all RequestActions
|
||||||
|
*/
|
||||||
|
export type RequestAction
|
||||||
|
= RequestConfigureAction
|
||||||
|
| RequestExecuteAction
|
||||||
|
| RequestCompleteAction;
|
69
src/app/core/data/request.effects.ts
Normal file
69
src/app/core/data/request.effects.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Injectable, Inject } from "@angular/core";
|
||||||
|
import { Actions, Effect } from "@ngrx/effects";
|
||||||
|
import { Store } from "@ngrx/store";
|
||||||
|
import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
|
||||||
|
import { ObjectCacheService } from "../cache/object-cache.service";
|
||||||
|
import { DSpaceRESTV2Response } from "../dspace-rest-v2/dspace-rest-v2-response.model";
|
||||||
|
import { DSpaceRESTv2Serializer } from "../dspace-rest-v2/dspace-rest-v2.serializer";
|
||||||
|
import { CacheableObject } from "../cache/object-cache.reducer";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
import { Response, SuccessResponse, ErrorResponse } from "../cache/response-cache.models";
|
||||||
|
import { hasNoValue } from "../../shared/empty.util";
|
||||||
|
import { GlobalConfig, GLOBAL_CONFIG } from "../../../config";
|
||||||
|
import { RequestState, RequestEntry } from "./request.reducer";
|
||||||
|
import {
|
||||||
|
RequestActionTypes, RequestConfigureAction, RequestExecuteAction,
|
||||||
|
RequestCompleteAction
|
||||||
|
} from "./request.actions";
|
||||||
|
import { ResponseCacheService } from "../cache/response-cache.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RequestEffects {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
|
||||||
|
private actions$: Actions,
|
||||||
|
private restApi: DSpaceRESTv2Service,
|
||||||
|
private objectCache: ObjectCacheService,
|
||||||
|
private responseCache: ResponseCacheService,
|
||||||
|
private store: Store<RequestState>
|
||||||
|
) { }
|
||||||
|
|
||||||
|
@Effect() execute = this.actions$
|
||||||
|
.ofType(RequestActionTypes.EXECUTE)
|
||||||
|
.flatMap((action: RequestExecuteAction) => {
|
||||||
|
return this.store.select<RequestEntry>('core', 'data', 'request', action.payload)
|
||||||
|
.take(1);
|
||||||
|
})
|
||||||
|
.flatMap((entry: RequestEntry) => {
|
||||||
|
const [ifArray, ifNotArray] = this.restApi.get(entry.request.href)
|
||||||
|
.share() // share ensures restApi.get() doesn't get called twice when the partitions are used below
|
||||||
|
.partition((data: DSpaceRESTV2Response) => Array.isArray(data._embedded));
|
||||||
|
|
||||||
|
return Observable.merge(
|
||||||
|
|
||||||
|
ifArray.map((data: DSpaceRESTV2Response) => {
|
||||||
|
return new DSpaceRESTv2Serializer(entry.request.resourceType).deserializeArray(data);
|
||||||
|
}).do((cos: CacheableObject[]) => cos.forEach((t) => this.addToObjectCache(t)))
|
||||||
|
.map((cos: Array<CacheableObject>): Array<string> => cos.map(t => t.uuid)),
|
||||||
|
|
||||||
|
ifNotArray.map((data: DSpaceRESTV2Response) => {
|
||||||
|
return new DSpaceRESTv2Serializer(entry.request.resourceType).deserialize(data);
|
||||||
|
}).do((co: CacheableObject) => this.addToObjectCache(co))
|
||||||
|
.map((co: CacheableObject): Array<string> => [co.uuid])
|
||||||
|
|
||||||
|
).map((ids: Array<string>) => new SuccessResponse(ids))
|
||||||
|
.do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
|
||||||
|
.map((response: Response) => new RequestCompleteAction(entry.request.href))
|
||||||
|
.catch((error: Error) => Observable.of(new ErrorResponse(error.message))
|
||||||
|
.do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
|
||||||
|
.map((response: Response) => new RequestCompleteAction(entry.request.href)));
|
||||||
|
});
|
||||||
|
|
||||||
|
protected addToObjectCache(co: CacheableObject): void {
|
||||||
|
if (hasNoValue(co) || hasNoValue(co.uuid)) {
|
||||||
|
throw new Error('The server returned an invalid object');
|
||||||
|
}
|
||||||
|
this.objectCache.add(co, this.EnvConfig.cache.msToLive);
|
||||||
|
}
|
||||||
|
}
|
32
src/app/core/data/request.models.ts
Normal file
32
src/app/core/data/request.models.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { SortOptions } from "../shared/sort-options.model";
|
||||||
|
import { PaginationOptions } from "../shared/pagination-options.model";
|
||||||
|
import { GenericConstructor } from "../shared/generic-constructor";
|
||||||
|
|
||||||
|
export class Request<T> {
|
||||||
|
constructor(
|
||||||
|
public href: string,
|
||||||
|
public resourceType: GenericConstructor<T>
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FindByIDRequest<T> extends Request<T> {
|
||||||
|
constructor(
|
||||||
|
href: string,
|
||||||
|
resourceType: GenericConstructor<T>,
|
||||||
|
public resourceID: string
|
||||||
|
) {
|
||||||
|
super(href, resourceType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FindAllRequest<T> extends Request<T> {
|
||||||
|
constructor(
|
||||||
|
href: string,
|
||||||
|
resourceType: GenericConstructor<T>,
|
||||||
|
public scopeID?: string,
|
||||||
|
public paginationOptions?: PaginationOptions,
|
||||||
|
public sortOptions?: SortOptions
|
||||||
|
) {
|
||||||
|
super(href, resourceType);
|
||||||
|
}
|
||||||
|
}
|
81
src/app/core/data/request.reducer.ts
Normal file
81
src/app/core/data/request.reducer.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { CacheableObject } from "../cache/object-cache.reducer";
|
||||||
|
import {
|
||||||
|
RequestActionTypes, RequestAction, RequestConfigureAction,
|
||||||
|
RequestExecuteAction, RequestCompleteAction
|
||||||
|
} from "./request.actions";
|
||||||
|
import { Request } from "./request.models";
|
||||||
|
|
||||||
|
export class RequestEntry {
|
||||||
|
request: Request<CacheableObject>;
|
||||||
|
requestPending: boolean;
|
||||||
|
responsePending: boolean;
|
||||||
|
completed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface RequestState {
|
||||||
|
[key: string]: RequestEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
||||||
|
const initialState = Object.create(null);
|
||||||
|
|
||||||
|
export const requestReducer = (state = initialState, action: RequestAction): RequestState => {
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case RequestActionTypes.CONFIGURE: {
|
||||||
|
return configureRequest(state, <RequestConfigureAction> action);
|
||||||
|
}
|
||||||
|
|
||||||
|
case RequestActionTypes.EXECUTE: {
|
||||||
|
return executeRequest(state, <RequestExecuteAction> action);
|
||||||
|
}
|
||||||
|
|
||||||
|
case RequestActionTypes.COMPLETE: {
|
||||||
|
return completeRequest(state, <RequestCompleteAction> action);
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function configureRequest(state: RequestState, action: RequestConfigureAction): RequestState {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.payload.href]: {
|
||||||
|
request: action.payload,
|
||||||
|
requestPending: true,
|
||||||
|
responsePending: false,
|
||||||
|
completed: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeRequest(state: RequestState, action: RequestExecuteAction): RequestState {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.payload]: Object.assign({}, state[action.payload], {
|
||||||
|
requestPending: false,
|
||||||
|
responsePending: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a request with the response
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* a RequestCompleteAction
|
||||||
|
* @return RequestState
|
||||||
|
* the new state, with the response added to the request
|
||||||
|
*/
|
||||||
|
function completeRequest(state: RequestState, action: RequestCompleteAction): RequestState {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.payload]: Object.assign({}, state[action.payload], {
|
||||||
|
responsePending: false,
|
||||||
|
completed: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
@@ -55,7 +55,8 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> {
|
|||||||
if (Array.isArray(response._embedded)) {
|
if (Array.isArray(response._embedded)) {
|
||||||
throw new Error('Expected a single model, use deserializeArray() instead');
|
throw new Error('Expected a single model, use deserializeArray() instead');
|
||||||
}
|
}
|
||||||
return <T> Deserialize(response._embedded, this.modelType);
|
let normalized = Object.assign({}, response._embedded, this.normalizeLinks(response._links));
|
||||||
|
return <T> Deserialize(normalized, this.modelType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,7 +71,26 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> {
|
|||||||
if (!Array.isArray(response._embedded)) {
|
if (!Array.isArray(response._embedded)) {
|
||||||
throw new Error('Expected an Array, use deserialize() instead');
|
throw new Error('Expected an Array, use deserialize() instead');
|
||||||
}
|
}
|
||||||
return <Array<T>> Deserialize(response._embedded, this.modelType);
|
let normalized = response._embedded.map((resource) => {
|
||||||
|
return Object.assign({}, resource, this.normalizeLinks(resource._links));
|
||||||
|
});
|
||||||
|
|
||||||
|
return <Array<T>> Deserialize(normalized, this.modelType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeLinks(links:any): any {
|
||||||
|
let normalizedLinks = links;
|
||||||
|
for (let link in normalizedLinks) {
|
||||||
|
if (Array.isArray(normalizedLinks[link])) {
|
||||||
|
normalizedLinks[link] = normalizedLinks[link].map(linkedResource => {
|
||||||
|
return {'self': linkedResource.href };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
normalizedLinks[link] = normalizedLinks[link].href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return normalizedLinks;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
58
src/app/core/index/href-index.actions.ts
Normal file
58
src/app/core/index/href-index.actions.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Action } from "@ngrx/store";
|
||||||
|
import { type } from "../../shared/ngrx/type";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of HrefIndexAction type definitions
|
||||||
|
*/
|
||||||
|
export const HrefIndexActionTypes = {
|
||||||
|
ADD: type('dspace/core/index/href/ADD'),
|
||||||
|
REMOVE_UUID: type('dspace/core/index/href/REMOVE_UUID')
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to add an href to the index
|
||||||
|
*/
|
||||||
|
export class AddToHrefIndexAction implements Action {
|
||||||
|
type = HrefIndexActionTypes.ADD;
|
||||||
|
payload: {
|
||||||
|
href: string;
|
||||||
|
uuid: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new AddToHrefIndexAction
|
||||||
|
*
|
||||||
|
* @param href
|
||||||
|
* the href to add
|
||||||
|
* @param uuid
|
||||||
|
* the uuid of the resource the href links to
|
||||||
|
*/
|
||||||
|
constructor(href: string, uuid: string) {
|
||||||
|
this.payload = { href, uuid };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to remove an href from the index
|
||||||
|
*/
|
||||||
|
export class RemoveUUIDFromHrefIndexAction implements Action {
|
||||||
|
type = HrefIndexActionTypes.REMOVE_UUID;
|
||||||
|
payload: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new RemoveUUIDFromHrefIndexAction
|
||||||
|
*
|
||||||
|
* @param uuid
|
||||||
|
* the uuid to remove all hrefs for
|
||||||
|
*/
|
||||||
|
constructor(uuid: string) {
|
||||||
|
this.payload = uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type to encompass all HrefIndexActions
|
||||||
|
*/
|
||||||
|
export type HrefIndexAction
|
||||||
|
= AddToHrefIndexAction
|
||||||
|
| RemoveUUIDFromHrefIndexAction;
|
32
src/app/core/index/href-index.effects.ts
Normal file
32
src/app/core/index/href-index.effects.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { Effect, Actions } from "@ngrx/effects";
|
||||||
|
import {
|
||||||
|
ObjectCacheActionTypes, AddToObjectCacheAction,
|
||||||
|
RemoveFromObjectCacheAction
|
||||||
|
} from "../cache/object-cache.actions";
|
||||||
|
import { AddToHrefIndexAction, RemoveUUIDFromHrefIndexAction } from "./href-index.actions";
|
||||||
|
import { hasValue } from "../../shared/empty.util";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HrefIndexEffects {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private actions$: Actions
|
||||||
|
) { }
|
||||||
|
|
||||||
|
@Effect() add$ = this.actions$
|
||||||
|
.ofType(ObjectCacheActionTypes.ADD)
|
||||||
|
.filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.self))
|
||||||
|
.map((action: AddToObjectCacheAction) => {
|
||||||
|
return new AddToHrefIndexAction(
|
||||||
|
action.payload.objectToCache.self,
|
||||||
|
action.payload.objectToCache.uuid
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
@Effect() remove$ = this.actions$
|
||||||
|
.ofType(ObjectCacheActionTypes.REMOVE)
|
||||||
|
.map((action: RemoveFromObjectCacheAction) => {
|
||||||
|
return new RemoveUUIDFromHrefIndexAction(action.payload);
|
||||||
|
});
|
||||||
|
}
|
43
src/app/core/index/href-index.reducer.ts
Normal file
43
src/app/core/index/href-index.reducer.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
HrefIndexAction, HrefIndexActionTypes, AddToHrefIndexAction,
|
||||||
|
RemoveUUIDFromHrefIndexAction
|
||||||
|
} from "./href-index.actions";
|
||||||
|
export interface HrefIndexState {
|
||||||
|
[href: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
||||||
|
const initialState: HrefIndexState = Object.create(null);
|
||||||
|
|
||||||
|
export const hrefIndexReducer = (state = initialState, action: HrefIndexAction): HrefIndexState => {
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case HrefIndexActionTypes.ADD: {
|
||||||
|
return addToHrefIndex(state, <AddToHrefIndexAction>action);
|
||||||
|
}
|
||||||
|
|
||||||
|
case HrefIndexActionTypes.REMOVE_UUID: {
|
||||||
|
return removeUUIDFromHrefIndex(state, <RemoveUUIDFromHrefIndexAction>action)
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function addToHrefIndex(state: HrefIndexState, action: AddToHrefIndexAction): HrefIndexState {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.payload.href]: action.payload.uuid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeUUIDFromHrefIndex(state: HrefIndexState, action: RemoveUUIDFromHrefIndexAction): HrefIndexState {
|
||||||
|
let newState = Object.create(null);
|
||||||
|
for (let href in state) {
|
||||||
|
if (state[href] !== action.payload) {
|
||||||
|
newState[href] = state[href];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newState;
|
||||||
|
}
|
14
src/app/core/index/index.reducers.ts
Normal file
14
src/app/core/index/index.reducers.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { combineReducers } from "@ngrx/store";
|
||||||
|
import { HrefIndexState, hrefIndexReducer } from "./href-index.reducer";
|
||||||
|
|
||||||
|
export interface IndexState {
|
||||||
|
href: HrefIndexState
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducers = {
|
||||||
|
href: hrefIndexReducer
|
||||||
|
};
|
||||||
|
|
||||||
|
export function indexReducer(state: any, action: any) {
|
||||||
|
return combineReducers(reducers)(state, action);
|
||||||
|
}
|
@@ -1,6 +1,8 @@
|
|||||||
import { autoserialize, inheritSerialization } from "cerialize";
|
import { autoserialize, inheritSerialization, autoserializeAs } from "cerialize";
|
||||||
import { DSpaceObject } from "./dspace-object.model";
|
import { DSpaceObject } from "./dspace-object.model";
|
||||||
import { Bitstream } from "./bitstream.model";
|
import { Bitstream } from "./bitstream.model";
|
||||||
|
import { Item } from "./item.model";
|
||||||
|
import { RemoteData } from "../data/remote-data";
|
||||||
|
|
||||||
@inheritSerialization(DSpaceObject)
|
@inheritSerialization(DSpaceObject)
|
||||||
export class Collection extends DSpaceObject {
|
export class Collection extends DSpaceObject {
|
||||||
@@ -54,7 +56,7 @@ export class Collection extends DSpaceObject {
|
|||||||
/**
|
/**
|
||||||
* The Bitstream that represents the logo of this Collection
|
* The Bitstream that represents the logo of this Collection
|
||||||
*/
|
*/
|
||||||
logo: Bitstream;
|
logo: RemoteData<Bitstream>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of Collections that are direct parents of this Collection
|
* An array of Collections that are direct parents of this Collection
|
||||||
@@ -66,4 +68,7 @@ export class Collection extends DSpaceObject {
|
|||||||
*/
|
*/
|
||||||
owner: Collection;
|
owner: Collection;
|
||||||
|
|
||||||
|
@autoserializeAs(RemoteData)
|
||||||
|
items: RemoteData<Item[]>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,9 @@ import { CacheableObject } from "../cache/object-cache.reducer";
|
|||||||
*/
|
*/
|
||||||
export abstract class DSpaceObject implements CacheableObject {
|
export abstract class DSpaceObject implements CacheableObject {
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
self: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The human-readable identifier of this DSpaceObject
|
* The human-readable identifier of this DSpaceObject
|
||||||
*/
|
*/
|
||||||
|
@@ -1,58 +0,0 @@
|
|||||||
import { ParamHash } from "./param-hash";
|
|
||||||
describe("ParamHash", () => {
|
|
||||||
|
|
||||||
it("should return a hash for a set of parameters", () => {
|
|
||||||
const hash = new ParamHash('azerty', true, 23).toString();
|
|
||||||
|
|
||||||
expect(hash).not.toBeNull();
|
|
||||||
expect(hash).not.toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should work with both simple and complex objects as parameters", () => {
|
|
||||||
const hash = new ParamHash('azerty', true, 23, { "a": { "b": ['azerty', true] }, "c": 23 }).toString();
|
|
||||||
|
|
||||||
expect(hash).not.toBeNull();
|
|
||||||
expect(hash).not.toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should work with null or undefined as parameters", () => {
|
|
||||||
const hash1 = new ParamHash(undefined).toString();
|
|
||||||
const hash2 = new ParamHash(null).toString();
|
|
||||||
const hash3 = new ParamHash(undefined, null).toString();
|
|
||||||
|
|
||||||
expect(hash1).not.toBeNull();
|
|
||||||
expect(hash1).not.toBe('');
|
|
||||||
expect(hash2).not.toBeNull();
|
|
||||||
expect(hash2).not.toBe('');
|
|
||||||
expect(hash3).not.toBeNull();
|
|
||||||
expect(hash3).not.toBe('');
|
|
||||||
expect(hash1).not.toEqual(hash2);
|
|
||||||
expect(hash1).not.toEqual(hash3);
|
|
||||||
expect(hash2).not.toEqual(hash3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should work if created without parameters", () => {
|
|
||||||
const hash1 = new ParamHash().toString();
|
|
||||||
const hash2 = new ParamHash().toString();
|
|
||||||
|
|
||||||
expect(hash1).not.toBeNull();
|
|
||||||
expect(hash1).not.toBe('');
|
|
||||||
expect(hash1).toEqual(hash2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create the same hash if created with the same set of parameters in the same order", () => {
|
|
||||||
const params = ['azerty', true, 23, { "a": { "b": ['azerty', true] }, "c": 23 }];
|
|
||||||
const hash1 = new ParamHash(...params).toString();
|
|
||||||
const hash2 = new ParamHash(...params).toString();
|
|
||||||
|
|
||||||
expect(hash1).toEqual(hash2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create a different hash if created with the same set of parameters in a different order", () => {
|
|
||||||
const params = ['azerty', true, 23, { "a": { "b": ['azerty', true] }, "c": 23 }];
|
|
||||||
const hash1 = new ParamHash(...params).toString();
|
|
||||||
const hash2 = new ParamHash(...params.reverse()).toString();
|
|
||||||
|
|
||||||
expect(hash1).not.toEqual(hash2);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,35 +0,0 @@
|
|||||||
import { Md5 } from "ts-md5/dist/md5";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a hash of a set of parameters
|
|
||||||
*/
|
|
||||||
export class ParamHash {
|
|
||||||
private params: Array<any>;
|
|
||||||
|
|
||||||
constructor(...params) {
|
|
||||||
this.params = params;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an md5 hash based on the
|
|
||||||
* params passed to the constructor
|
|
||||||
*
|
|
||||||
* If you hash the same set of params in the
|
|
||||||
* same order the hashes will be identical
|
|
||||||
*
|
|
||||||
* @return {string}
|
|
||||||
* an md5 hash
|
|
||||||
*/
|
|
||||||
toString(): string {
|
|
||||||
let hash = new Md5();
|
|
||||||
this.params.forEach((param) => {
|
|
||||||
if (param === Object(param)) {
|
|
||||||
hash.appendStr(JSON.stringify(param));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
hash.appendStr('' + param);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return hash.end().toString();
|
|
||||||
}
|
|
||||||
}
|
|
10
src/app/core/shared/self-link.model.ts
Normal file
10
src/app/core/shared/self-link.model.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { autoserialize } from "cerialize";
|
||||||
|
|
||||||
|
export class SelfLink {
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
self: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
uuid: string;
|
||||||
|
}
|
@@ -8,7 +8,7 @@ import { GlobalConfig } from "../../../config";
|
|||||||
* TODO write tests once GlobalConfig becomes injectable
|
* TODO write tests once GlobalConfig becomes injectable
|
||||||
*/
|
*/
|
||||||
export class UIURLCombiner extends URLCombiner{
|
export class UIURLCombiner extends URLCombiner{
|
||||||
constructor(...parts:Array<string>) {
|
constructor(EnvConfig: GlobalConfig, ...parts: Array<string>) {
|
||||||
super(GlobalConfig.ui.baseURL, GlobalConfig.ui.nameSpace, ...parts);
|
super(EnvConfig.ui.baseUrl, EnvConfig.ui.nameSpace, ...parts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,22 @@
|
|||||||
<div class="home">
|
<div class="home">
|
||||||
Home component
|
<div *ngIf="collections">
|
||||||
|
<h3>Collections</h3>
|
||||||
|
<p *ngIf="(collections.isLoading | async)">Loading…</p>
|
||||||
|
<p *ngIf="(collections.hasFailed | async)">Failed: {{(remoteData.errorMessage | async)}}</p>
|
||||||
|
<ul *ngIf="(collections.hasSucceeded | async)">
|
||||||
|
<li *ngFor="let collection of (collections.payload | async)">
|
||||||
|
{{collection?.name}}<br>
|
||||||
|
<span class="text-muted">{{collection?.shortDescription}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="items">
|
||||||
|
<h3>Items</h3>
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let item of (items.payload | async)">
|
||||||
|
{{item?.name}}<br>
|
||||||
|
<span class="text-muted">{{item?.findMetadata('dc.description.abstract')}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,4 +1,11 @@
|
|||||||
import { Component, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
import { Component, ChangeDetectionStrategy, ViewEncapsulation, OnInit } from '@angular/core';
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
import { Collection } from "../core/shared/collection.model";
|
||||||
|
import { Item } from "../core/shared/item.model";
|
||||||
|
import { CollectionDataService } from "../core/data/collection-data.service";
|
||||||
|
import { ItemDataService } from "../core/data/item-data.service";
|
||||||
|
import { ObjectCacheService } from "../core/cache/object-cache.service";
|
||||||
|
import { RemoteData } from "../core/data/remote-data";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
changeDetection: ChangeDetectionStrategy.Default,
|
changeDetection: ChangeDetectionStrategy.Default,
|
||||||
@@ -7,11 +14,16 @@ import { Component, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/
|
|||||||
styleUrls: ['./home.component.css'],
|
styleUrls: ['./home.component.css'],
|
||||||
templateUrl: './home.component.html'
|
templateUrl: './home.component.html'
|
||||||
})
|
})
|
||||||
export class HomeComponent {
|
export class HomeComponent implements OnInit {
|
||||||
|
|
||||||
data: any = {};
|
data: any = {};
|
||||||
|
collections: RemoteData<Collection[]>;
|
||||||
|
items: RemoteData<Item[]>;
|
||||||
|
|
||||||
constructor() {
|
constructor(
|
||||||
|
private cds: CollectionDataService,
|
||||||
|
private ids: ItemDataService,
|
||||||
|
private objectCache: ObjectCacheService
|
||||||
|
) {
|
||||||
this.universalInit();
|
this.universalInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,4 +31,13 @@ export class HomeComponent {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.collections = this.cds.findAll();
|
||||||
|
this.items = this.ids.findAll();
|
||||||
|
this.cds.findById('5179').payload.subscribe(o => console.log('collection 1', o));
|
||||||
|
this.cds.findById('6547').payload.subscribe(o => console.log('collection 2', o));
|
||||||
|
this.ids.findById('8871').payload.subscribe(o => console.log('item 1', o));
|
||||||
|
this.ids.findById('9978').payload.subscribe(o => console.log('item 2', o));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -2,9 +2,11 @@ import { NgModule } from '@angular/core';
|
|||||||
|
|
||||||
import { HomeComponent } from './home.component';
|
import { HomeComponent } from './home.component';
|
||||||
import { HomeRoutingModule } from './home-routing.module';
|
import { HomeRoutingModule } from './home-routing.module';
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
CommonModule,
|
||||||
HomeRoutingModule
|
HomeRoutingModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
|
Reference in New Issue
Block a user