From c1013183523fa8140c6df8a568eb93e0113c946a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20=C5=BBydek?= Date: Wed, 1 Nov 2023 15:12:08 +0200 Subject: [PATCH] feat: add `cacheHit` flag to response that indicates if it was retrieved from cache --- README.md | 10 ++++++++ __tests__/client.test.ts | 49 ++++++++++++++++++++++++++++++++++++---- src/client.ts | 26 +++++++++++++++------ src/global.ts | 5 ++++ 4 files changed, 78 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index fc2fd73..ea0f791 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,16 @@ const fpjsClient = new FpjsClient({ Cache keys are based on the combination of _GetOptions_. For example, API responses for calls with `extendedResult: true` and `extendedResult: false` are stored independently. +You can check if your response was retrieved from cache like this: +```js + +const result = await fpjsClient.getVisitorData({ extendedResult: true }) + +// true if cache was hit +console.log(result.cacheHit) + +``` + #### Creating a custom cache The SDK can use a custom cache store implemented inside your application. This is useful when a different data store is more convenient in your environment, such as a hybrid mobile app. diff --git a/__tests__/client.test.ts b/__tests__/client.test.ts index 18f934e..6a6d5f4 100644 --- a/__tests__/client.test.ts +++ b/__tests__/client.test.ts @@ -235,6 +235,29 @@ describe(`SPA client`, () => { expect(result2?.visitorId).toBe(mockVisitorId) }) + it('should return cached response on second call if it exists', async () => { + const client = new FpjsClient({ + loadOptions: getDefaultLoadOptions(), + cacheLocation: CacheLocation.LocalStorage, + }) + await client.init() + + const result1 = await client.getVisitorData() + expect(result1).toEqual({ + visitorId: mockVisitorId, + cacheHit: false, + }) + + const result2 = await client.getVisitorData() + + expect(result2).toEqual({ + visitorId: mockVisitorId, + cacheHit: true, + }) + + expect(agentGetMock).toHaveBeenCalledTimes(1) + }) + it('should remove in-flight request even if it throws', async () => { agentGetMock.mockReset().mockRejectedValue(new Error()) @@ -251,6 +274,7 @@ describe(`SPA client`, () => { await expect(client.getVisitorData({}, true)).resolves.toEqual({ visitorId: mockVisitorId, + cacheHit: false, }) expect(agentGetMock).toHaveBeenCalledTimes(2) @@ -280,8 +304,14 @@ describe(`SPA client`, () => { const result2 = await getCall2 expect(agentGetMock).toHaveBeenCalledTimes(0) - expect(result1?.visitorId).toBe(mockVisitorId) - expect(result2?.visitorId).toBe(mockVisitorId) + expect(result1).toEqual({ + visitorId: mockVisitorId, + cacheHit: true, + }) + expect(result2).toEqual({ + visitorId: mockVisitorId, + cacheHit: true, + }) }) it(`shouldn't get cached data if there is an in-flight request already if options are different`, async () => { @@ -308,8 +338,14 @@ describe(`SPA client`, () => { const result2 = await getCall2 expect(agentGetMock).toHaveBeenCalledTimes(1) - expect(result1?.visitorId).toBe(mockVisitorId) - expect(result2?.visitorId).toBe(mockVisitorId) + expect(result1).toEqual({ + visitorId: mockVisitorId, + cacheHit: true, + }) + expect(result2).toEqual({ + visitorId: mockVisitorId, + cacheHit: false, + }) }) it(`shouldn't get cached data if a flag to ignore cache is set to true`, async () => { @@ -332,7 +368,10 @@ describe(`SPA client`, () => { const result = await client.getVisitorData(options, true) expect(agentGetMock).toHaveBeenCalledTimes(1) - expect(result?.visitorId).toBe(mockVisitorId) + expect(result).toEqual({ + visitorId: mockVisitorId, + cacheHit: false, + }) }) }) diff --git a/src/client.ts b/src/client.ts index e122552..61f0a3c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -11,7 +11,7 @@ import { MAX_CACHE_LIFE, SessionStorageCache, } from './cache' -import { CacheLocation, FpjsClientOptions, VisitorData } from './global' +import { CacheLocation, FpjsClientOptions, FpjsSpaResponse, VisitorData } from './global' import * as packageInfo from '../package.json' const cacheLocationBuilders: Record ICache> = { @@ -158,7 +158,10 @@ export class FpjsClient { * @param options * @param ignoreCache if set to true a request to the API will be made even if the data is present in cache */ - public async getVisitorData(options: GetOptions = {}, ignoreCache = false) { + public async getVisitorData( + options: GetOptions = {}, + ignoreCache = false + ): Promise>> { const cacheKey = FpjsClient.makeCacheKey(options) const key = cacheKey.toKey() @@ -169,7 +172,7 @@ export class FpjsClient { this.inFlightRequests.set(key, promise) } - return (await this.inFlightRequests.get(key)) as VisitorData + return (await this.inFlightRequests.get(key)) as FpjsSpaResponse> } /** @@ -186,19 +189,28 @@ export class FpjsClient { return new CacheKey(options) } - private async _identify(options: GetOptions, ignoreCache = false) { + private async _identify( + options: GetOptions, + ignoreCache = false + ): Promise>> { const key = FpjsClient.makeCacheKey(options) if (!ignoreCache) { const cacheResult = await this.cacheManager.get(key) if (cacheResult) { - return cacheResult + return { + ...cacheResult, + cacheHit: true, + } } } - const agentResult = await this.agent.get(options) + const agentResult = (await this.agent.get(options)) as VisitorData await this.cacheManager.set(key, agentResult) - return agentResult + return { + ...agentResult, + cacheHit: false, + } } } diff --git a/src/global.ts b/src/global.ts index 3797d57..595ad38 100644 --- a/src/global.ts +++ b/src/global.ts @@ -5,6 +5,11 @@ export type VisitorData = TExtended extends f ? FingerprintJS.GetResult : FingerprintJS.ExtendedGetResult +export type FpjsSpaResponse = T & { + // Indicates whether the response was retrieved from cache. + cacheHit: boolean +} + export enum CacheLocation { Memory = 'memory', LocalStorage = 'localstorage',