diff --git a/src/EmberNexus.ts b/src/EmberNexus.ts index 036cb5b..67720d2 100644 --- a/src/EmberNexus.ts +++ b/src/EmberNexus.ts @@ -1,5 +1,6 @@ import 'reflect-metadata'; +import { LRUCache } from 'lru-cache'; import { Container } from 'typedi'; import GetElementEndpoint from '~/Endpoint/Element/GetElementEndpoint'; @@ -10,8 +11,31 @@ import { Relation } from '~/Type/Definition/Relation'; import { Uuid } from '~/Type/Definition/Uuid'; class EmberNexus { - test(uuid: Uuid): Promise { - return Container.get(GetElementEndpoint).getElement(uuid); + private cache: LRUCache; + + constructor() { + this.cache = new LRUCache({ + max: Container.get(WebSdkConfiguration).getElementCacheMaxEntries(), + }); + } + + getElement(uuid: Uuid): Promise { + return new Promise((resolve) => { + if (this.cache.has(uuid)) { + const element = this.cache.get(uuid); + if (element !== undefined) { + return resolve(element); + } + } + return resolve( + Container.get(GetElementEndpoint) + .getElement(uuid) + .then((element) => { + this.cache.set(uuid, element); + return element; + }), + ); + }); } } diff --git a/src/Service/WebSdkConfiguration.ts b/src/Service/WebSdkConfiguration.ts index d1922cc..9602455 100644 --- a/src/Service/WebSdkConfiguration.ts +++ b/src/Service/WebSdkConfiguration.ts @@ -6,10 +6,12 @@ import { Token } from '~/Type/Definition/Token'; class WebSdkConfiguration { private token: Token | null; private apiHost: string; + private elementCacheMaxEntries: number; constructor() { this.token = null; this.apiHost = ''; + this.elementCacheMaxEntries = 100; } hasToken(): boolean { @@ -30,6 +32,15 @@ class WebSdkConfiguration { this.apiHost = apiHost; return this; } + + getElementCacheMaxEntries(): number { + return this.elementCacheMaxEntries; + } + + setElementCacheMaxEntries(value: number): WebSdkConfiguration { + this.elementCacheMaxEntries = value; + return this; + } } export { WebSdkConfiguration }; diff --git a/test/Feature/EmberNexus/EmberNexusGetElementNode.test.ts b/test/Feature/EmberNexus/EmberNexusGetElementNode.test.ts new file mode 100644 index 0000000..a1a699d --- /dev/null +++ b/test/Feature/EmberNexus/EmberNexusGetElementNode.test.ts @@ -0,0 +1,62 @@ +import { expect } from 'chai'; +import { HttpResponse, http } from 'msw'; +// eslint-disable-next-line import/no-unresolved +import { setupServer } from 'msw/node'; +import { Container } from 'typedi'; + +import { EmberNexus } from '~/EmberNexus'; +import { Logger } from '~/Service/Logger'; +import { WebSdkConfiguration } from '~/Service/WebSdkConfiguration'; +import { validateUuidFromString } from '~/Type/Definition/Uuid'; + +import { TestLogger } from '../TestLogger'; + +const mockServer = setupServer( + http.get('http://mock-api/2ce13d4f-bd96-4b71-b4e7-d43bd9948836', () => { + return HttpResponse.json( + { + type: 'Data', + id: '2ce13d4f-bd96-4b71-b4e7-d43bd9948836', + data: { + created: '2023-10-06T20:27:56+00:00', + updated: '2023-10-06T20:27:56+00:00', + name: 'Test Data', + }, + }, + { + status: 200, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }, + ); + }), +); + +const testLogger: TestLogger = new TestLogger(); +Container.set(Logger, testLogger); +Container.get(WebSdkConfiguration).setApiHost('http://mock-api'); + +test('EmberNexus should handle node request', async () => { + mockServer.listen(); + const uuid = validateUuidFromString('2ce13d4f-bd96-4b71-b4e7-d43bd9948836'); + + const emberNexus = new EmberNexus(); + const node = await emberNexus.getElement(uuid); + + expect( + testLogger.assertDebugHappened( + 'Executing HTTP GET request against url http://mock-api/2ce13d4f-bd96-4b71-b4e7-d43bd9948836 .', + ), + ).to.be.true; + + expect(node).to.have.keys('id', 'type', 'data'); + expect(node).to.not.have.keys('start', 'end'); + expect(node.id).to.equal(uuid); + expect(node.type).to.equal('Data'); + expect(node.data.created).to.be.instanceof(Date); + expect(node.data.updated).to.be.instanceof(Date); + expect(Object.keys(node.data)).to.have.lengthOf(3); + + mockServer.close(); +}); diff --git a/test/Feature/EmberNexus/EmberNexusGetElementNodeWithCache.test.ts b/test/Feature/EmberNexus/EmberNexusGetElementNodeWithCache.test.ts new file mode 100644 index 0000000..e70a957 --- /dev/null +++ b/test/Feature/EmberNexus/EmberNexusGetElementNodeWithCache.test.ts @@ -0,0 +1,57 @@ +import { expect } from 'chai'; +import { HttpResponse, http } from 'msw'; +// eslint-disable-next-line import/no-unresolved +import { setupServer } from 'msw/node'; +import { Container } from 'typedi'; + +import { EmberNexus } from '~/EmberNexus'; +import { Logger } from '~/Service/Logger'; +import { WebSdkConfiguration } from '~/Service/WebSdkConfiguration'; +import { validateUuidFromString } from '~/Type/Definition/Uuid'; + +import { TestLogger } from '../TestLogger'; + +const mockServer = setupServer( + http.get('http://mock-api/2ce13d4f-bd96-4b71-b4e7-d43bd9948836', () => { + return HttpResponse.json( + { + type: 'Data', + id: '2ce13d4f-bd96-4b71-b4e7-d43bd9948836', + data: { + created: '2023-10-06T20:27:56+00:00', + updated: '2023-10-06T20:27:56+00:00', + name: 'Test Data', + }, + }, + { + status: 200, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }, + ); + }), +); + +const testLogger: TestLogger = new TestLogger(); +Container.set(Logger, testLogger); +Container.get(WebSdkConfiguration).setApiHost('http://mock-api'); + +test('EmberNexus should use the cache for repeated node requests', async () => { + mockServer.listen(); + const uuid = validateUuidFromString('2ce13d4f-bd96-4b71-b4e7-d43bd9948836'); + + const emberNexus = new EmberNexus(); + const node1 = await emberNexus.getElement(uuid); + const node2 = await emberNexus.getElement(uuid); + + expect( + testLogger.assertDebugHappened( + 'Executing HTTP GET request against url http://mock-api/2ce13d4f-bd96-4b71-b4e7-d43bd9948836 .', + ), + ).to.be.true; + + expect(node1).to.be.deep.equal(node2); + + mockServer.close(); +}); diff --git a/test/Feature/EmberNexus/EmberNexusGetElementRelation.test.ts b/test/Feature/EmberNexus/EmberNexusGetElementRelation.test.ts new file mode 100644 index 0000000..04418b1 --- /dev/null +++ b/test/Feature/EmberNexus/EmberNexusGetElementRelation.test.ts @@ -0,0 +1,63 @@ +import { expect } from 'chai'; +import { HttpResponse, http } from 'msw'; +// eslint-disable-next-line import/no-unresolved +import { setupServer } from 'msw/node'; +import { Container } from 'typedi'; + +import { EmberNexus } from '~/EmberNexus'; +import { Logger } from '~/Service/Logger'; +import { WebSdkConfiguration } from '~/Service/WebSdkConfiguration'; +import { validateUuidFromString } from '~/Type/Definition/Uuid'; + +import { TestLogger } from '../TestLogger'; + +const mockServer = setupServer( + http.get('http://mock-api/eb243613-832b-411a-a161-616cdd75e2f9', () => { + return HttpResponse.json( + { + type: 'RELATION', + id: 'eb243613-832b-411a-a161-616cdd75e2f9', + start: 'ab5a01aa-6a02-42a9-9613-69d9453d0064', + end: '0b624f56-a8ed-43f0-a250-4a821679a51f', + data: { + created: '2023-10-06T20:27:56+00:00', + updated: '2023-10-06T20:27:56+00:00', + name: 'Test relation', + }, + }, + { + status: 200, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }, + ); + }), +); + +const testLogger: TestLogger = new TestLogger(); +Container.set(Logger, testLogger); +Container.get(WebSdkConfiguration).setApiHost('http://mock-api'); + +test('EmberNexus should handle relation request', async () => { + mockServer.listen(); + const uuid = validateUuidFromString('eb243613-832b-411a-a161-616cdd75e2f9'); + + const emberNexus = new EmberNexus(); + const relation = await emberNexus.getElement(uuid); + + expect( + testLogger.assertDebugHappened( + 'Executing HTTP GET request against url http://mock-api/eb243613-832b-411a-a161-616cdd75e2f9 .', + ), + ).to.be.true; + + expect(relation).to.have.keys('id', 'start', 'end', 'type', 'data'); + expect(relation.id).to.equal(uuid); + expect(relation.type).to.equal('RELATION'); + expect(relation.data.created).to.be.instanceof(Date); + expect(relation.data.updated).to.be.instanceof(Date); + expect(Object.keys(relation.data)).to.have.lengthOf(3); + + mockServer.close(); +}); diff --git a/test/Feature/EmberNexus/EmberNexusGetElementRelationWithCache.test.ts b/test/Feature/EmberNexus/EmberNexusGetElementRelationWithCache.test.ts new file mode 100644 index 0000000..a75268b --- /dev/null +++ b/test/Feature/EmberNexus/EmberNexusGetElementRelationWithCache.test.ts @@ -0,0 +1,59 @@ +import { expect } from 'chai'; +import { HttpResponse, http } from 'msw'; +// eslint-disable-next-line import/no-unresolved +import { setupServer } from 'msw/node'; +import { Container } from 'typedi'; + +import { EmberNexus } from '~/EmberNexus'; +import { Logger } from '~/Service/Logger'; +import { WebSdkConfiguration } from '~/Service/WebSdkConfiguration'; +import { validateUuidFromString } from '~/Type/Definition/Uuid'; + +import { TestLogger } from '../TestLogger'; + +const mockServer = setupServer( + http.get('http://mock-api/32893cda-42db-4287-ac00-e73f250e1eeb', () => { + return HttpResponse.json( + { + type: 'RELATION', + id: '32893cda-42db-4287-ac00-e73f250e1eeb', + start: 'ab5a01aa-6a02-42a9-9613-69d9453d0064', + end: '0b624f56-a8ed-43f0-a250-4a821679a51f', + data: { + created: '2023-10-06T20:27:56+00:00', + updated: '2023-10-06T20:27:56+00:00', + name: 'Test relation', + }, + }, + { + status: 200, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }, + ); + }), +); + +const testLogger: TestLogger = new TestLogger(); +Container.set(Logger, testLogger); +Container.get(WebSdkConfiguration).setApiHost('http://mock-api'); + +test('EmberNexus should use the cache for repeated relation request', async () => { + mockServer.listen(); + const uuid = validateUuidFromString('32893cda-42db-4287-ac00-e73f250e1eeb'); + + const emberNexus = new EmberNexus(); + const relation1 = await emberNexus.getElement(uuid); + const relation2 = await emberNexus.getElement(uuid); + + expect( + testLogger.assertDebugHappened( + 'Executing HTTP GET request against url http://mock-api/32893cda-42db-4287-ac00-e73f250e1eeb .', + ), + ).to.be.true; + + expect(relation1).to.be.deep.equal(relation2); + + mockServer.close(); +}); diff --git a/test/Feature/TestLogger.ts b/test/Feature/TestLogger.ts index 2281530..f51852e 100644 --- a/test/Feature/TestLogger.ts +++ b/test/Feature/TestLogger.ts @@ -26,40 +26,44 @@ class TestLogger implements LoggerInterface { return undefined; } - assertDebugHappened(message: string): boolean { + assertDebugHappened(message: string, assertTimes: number = 1): boolean { + let timesHappened = 0; for (let i = 0; i < this.debugCalls.length; i++) { if (this.debugCalls[i][0] === message) { - return true; + timesHappened++; } } - return false; + return assertTimes === timesHappened; } - assertErrorHappened(message: string): boolean { + assertErrorHappened(message: string, assertTimes: number = 1): boolean { + let timesHappened = 0; for (let i = 0; i < this.errorCalls.length; i++) { if (this.errorCalls[i][0] === message) { - return true; + timesHappened++; } } - return false; + return assertTimes === timesHappened; } - assertInfoHappened(message: string): boolean { + assertInfoHappened(message: string, assertTimes: number = 1): boolean { + let timesHappened = 0; for (let i = 0; i < this.infoCalls.length; i++) { if (this.infoCalls[i][0] === message) { - return true; + timesHappened++; } } - return false; + return assertTimes === timesHappened; } - assertWarnHappened(message: string): boolean { + assertWarnHappened(message: string, assertTimes: number = 1): boolean { + let timesHappened = 0; for (let i = 0; i < this.warnCalls.length; i++) { if (this.warnCalls[i][0] === message) { - return true; + timesHappened++; } } - return false; + return assertTimes === timesHappened; } printAllCalls(): void {