From 60940d133ac730bc4f5836ccb375f5d3c267988f Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Fri, 30 Aug 2024 15:48:19 +0200 Subject: [PATCH] feat: Add caching mechanism for API requests --- frontend/app/api_client.ts | 113 +++++++++++++++++++++++++ frontend/app/services/MetricService.ts | 4 +- 2 files changed, 115 insertions(+), 2 deletions(-) diff --git a/frontend/app/api_client.ts b/frontend/app/api_client.ts index f6bb4dc49..2bb15f88f 100644 --- a/frontend/app/api_client.ts +++ b/frontend/app/api_client.ts @@ -56,8 +56,10 @@ export default class APIClient { private init: RequestInit; private readonly siteId: string | undefined; private refreshingTokenPromise: Promise | null = null; + private cacheManager = new CacheManager(); constructor() { + this.cacheManager.init(); const jwt = store.getState().getIn(['user', 'jwt']); const siteId = store.getState().getIn(['site', 'siteId']); this.init = { @@ -234,4 +236,115 @@ export default class APIClient { this.init.method = 'PATCH'; return this.fetch(path, params, 'PATCH'); } + + async cachedPost(path: string, params?: any, options?: any, headers?: Record): Promise { + if (params.startTimestamp) { + params.startTimestamp = roundToFiveMinutes(params.startTimestamp); + } + if (params.endTimestamp) { + params.endTimestamp = roundToFiveMinutes(params.endTimestamp); + } + const bodyHash = params ? simpleHash(JSON.stringify(params)) : 0; + const cacheKey = `${path}-${bodyHash}`; + + const cachedResponse = await this.cacheManager.get(cacheKey); + if (cachedResponse) { + return cachedResponse; + } + + const response = await this.post(path, params, options, headers); + if (response.ok) { + this.cacheManager.set(cacheKey, response.clone()); + } + + return response; + } + + async cachedGet(path: string, params?: any, options?: any, headers?: Record): Promise { + if (params.startTimestamp) { + params.startTimestamp = roundToFiveMinutes(params.startTimestamp); + } + if (params.endTimestamp) { + params.endTimestamp = roundToFiveMinutes(params.endTimestamp); + } + const bodyHash = params ? simpleHash(JSON.stringify(params)) : 0; + const cacheKey = `${path}-${bodyHash}`; + + const cachedResponse = await this.cacheManager.get(cacheKey); + if (cachedResponse) { + return cachedResponse; + } + const response = await this.post(path, params, options, headers); + if (response.ok) { + this.cacheManager.set(cacheKey, response.clone()); + } + + return response; + } +} + +function simpleHash(str: string): number { + let hash = 5381; + let i = str.length; + + while (i) { + hash = (hash * 33) ^ str.charCodeAt(--i); + } + + return hash >>> 0; +} + +function roundToFiveMinutes(timestamp: number): number { + const minutes = 5 * 60 * 1000; + return Math.floor(timestamp / minutes) * minutes; +} + +class CacheManager { + private cache: Cache | null = null; + private invalidationTracker = new Map(); + async init() { + this.cache = await caches.open('or-req-cache'); + this.restoreInvalidationTracker(); + } + + saveInvalidationTracker() { + localStorage.setItem('invalidationTracker', JSON.stringify(Array.from(this.invalidationTracker.entries()))); + } + + restoreInvalidationTracker() { + const tracker = localStorage.getItem('invalidationTracker'); + if (tracker) { + this.invalidationTracker = new Map(JSON.parse(tracker)); + } + } + + async get(key: string): Promise { + if (!this.cache) { + return null; + } + if (this.invalidationTracker.has(key)) { + const ts = this.invalidationTracker.get(key); + if (!ts) { + return null; + } + if (Date.now() - ts > 5 * 60 * 1000) { + this.cache.delete(key); + this.invalidationTracker.delete(key); + this.saveInvalidationTracker(); + return null; + } + } + + const result = await this.cache.match(key) + return result ?? null; + } + + async set(key: string, response: Response) { + if (!this.cache) { + return; + } + this.invalidationTracker.set(key, Date.now()); + this.saveInvalidationTracker(); + this.cache.put(key, response.clone()); + } } diff --git a/frontend/app/services/MetricService.ts b/frontend/app/services/MetricService.ts index 4e90ccb4d..d2c6f55b5 100644 --- a/frontend/app/services/MetricService.ts +++ b/frontend/app/services/MetricService.ts @@ -86,7 +86,7 @@ export default class MetricService { data.metricOf = 'sessionCount'; } try { - const r = await this.client.post(path, data); + const r = await this.client.cachedPost(path, data); const response = await r.json(); return response.data || {}; } catch (e) { @@ -114,7 +114,7 @@ export default class MetricService { filter.filters = drillDownFilter; } - let resp: Response = await this.client.post(`/cards/try/issues`, filter); + let resp: Response = await this.client.cachedPost(`/cards/try/issues`, filter); const json: any = await resp.json(); return await json.data || {}; }