diff --git a/src/app/core/cache/object-cache.service.spec.ts b/src/app/core/cache/object-cache.service.spec.ts index a05d6fdbef..827e39ab7e 100644 --- a/src/app/core/cache/object-cache.service.spec.ts +++ b/src/app/core/cache/object-cache.service.spec.ts @@ -72,9 +72,10 @@ describe("ObjectCacheService", () => { it("should not return a cached object that has exceeded its time to live", () => { spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry)); - let testObj: any; - service.get(uuid, TestClass).take(1).subscribe(o => testObj = o); - expect(testObj).toBeUndefined(); + let getObsHasFired = false; + const subscription = service.get(uuid, TestClass).subscribe(o => getObsHasFired = true); + expect(getObsHasFired).toBe(false); + subscription.unsubscribe(); }); }); diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 6cabbfd91d..9093093f50 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -51,6 +51,7 @@ export class ObjectCacheService { * @param type * The type of the object to get * @return Observable + * An observable of the requested object */ get(uuid: string, type: GenericConstructor): Observable { return this.store.select('core', 'cache', 'object', uuid) diff --git a/src/app/core/cache/request-cache.service.spec.ts b/src/app/core/cache/request-cache.service.spec.ts new file mode 100644 index 0000000000..eb4a07a742 --- /dev/null +++ b/src/app/core/cache/request-cache.service.spec.ts @@ -0,0 +1,147 @@ +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; + + 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(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) => { + 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) => { + 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); + }); + }); +}); diff --git a/src/app/core/cache/request-cache.service.ts b/src/app/core/cache/request-cache.service.ts index 418a1ad204..efa7b0d426 100644 --- a/src/app/core/cache/request-cache.service.ts +++ b/src/app/core/cache/request-cache.service.ts @@ -10,12 +10,35 @@ import { 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 ) {} + /** + * 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 + * an observable of the RequestCacheEntry for this request + */ findAll( key: string, service: OpaqueToken, @@ -29,6 +52,22 @@ export class RequestCacheService { 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 + * an observable of the RequestCacheEntry for this request + */ findById( key: string, service: OpaqueToken, @@ -40,12 +79,29 @@ export class RequestCacheService { 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 + * an observable of the RequestCacheEntry with the specified key + */ get(key: string): Observable { return this.store.select('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; @@ -56,6 +112,15 @@ export class RequestCacheService { 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;