From 6e2a585efd49a2ade6e3e3433da30592f929c3ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Thu, 9 Oct 2025 19:53:09 -0300 Subject: [PATCH 1/2] feat: implement etag on fetch entities actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/actions/fetch-entities-actions.js | 136 +++++++++++++------------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/src/actions/fetch-entities-actions.js b/src/actions/fetch-entities-actions.js index bb5b2f76..b298d80a 100644 --- a/src/actions/fetch-entities-actions.js +++ b/src/actions/fetch-entities-actions.js @@ -3,6 +3,50 @@ import SummitAPIRequest from "../utils/build-json/SummitAPIRequest"; import EventAPIRequest from "../utils/build-json/EventsAPIRequest"; import SpeakersAPIRequest from "../utils/build-json/SpeakersAPIRequest"; +const etagCache = {}; + +const byLowerCase = toFind => value => toLowerCase(value) === toFind; +const toLowerCase = value => value.toLowerCase(); +const getKeys = headers => Object.keys(headers); + +const getHeaderCaseInsensitive = (headerName, headers = {}) => { + const key = getKeys(headers).find(byLowerCase(headerName)); + return key ? headers[key] : undefined; +}; + +const fetchWithEtag = async (url, cacheKey) => { + const headers = {}; + + if (etagCache.hasOwnProperty(cacheKey)) { + const { etag } = etagCache[cacheKey]; + if (etag) { + headers['If-None-Match'] = etag; + } + } + + const res = await fetch(url, { + method: 'GET', + cache: "no-store", + headers, + }); + + if (res.status === 304 && etagCache.hasOwnProperty(cacheKey)) { + const { body } = etagCache[cacheKey]; + return body; + } + + if (res.status === 200) { + const data = await res.json(); + const responseETAG = getHeaderCaseInsensitive('etag', res.headers); + if (responseETAG) { + etagCache[cacheKey] = { etag: responseETAG, body: data }; + } + return data; + } + + return null; +}; + /** * @param summitId * @param eventId @@ -18,16 +62,9 @@ export const fetchEventById = async (summitId, eventId, accessToken = null) => { } const apiUrlWithParams = EventAPIRequest.build(apiUrl); + const cacheKey = apiUrlWithParams; - return fetch(apiUrlWithParams, { - method: 'GET', - cache: "no-store", - }).then(async (response) => { - if (response.status === 200) { - return await response.json(); - } - return null; - }); + return fetchWithEtag(apiUrlWithParams, cacheKey); } /** @@ -39,15 +76,10 @@ export const fetchEventById = async (summitId, eventId, accessToken = null) => { export const fetchStreamingInfoByEventId = async (summitId, eventId, accessToken) => { const apiUrl = URI(`${process.env.GATSBY_SUMMIT_API_BASE_URL}/api/v1/summits/${summitId}/events/${eventId}/published/streaming-info`); apiUrl.addQuery('access_token', accessToken); - return fetch(apiUrl.toString(), { - method: 'GET', - cache: "no-store", - }).then(async (response) => { - if (response.status === 200) { - return await response.json(); - } - return null; - }); + const url = apiUrl.toString(); + const cacheKey = url; + + return fetchWithEtag(url, cacheKey); } /** @@ -64,19 +96,13 @@ export const fetchEventTypeById = async (summitId, eventTypeId, accessToken = nu apiUrl.addQuery('access_token', accessToken); } - return fetch(apiUrl.toString(), { - method: 'GET', - cache: "no-store", - }).then(async (response) => { - if (response.status === 200) { - return await response.json(); - } - return null; - }); + const url = apiUrl.toString(); + const cacheKey = url; + + return fetchWithEtag(url, cacheKey); } /** - * * @param summitId * @param locationId * @param expand @@ -94,19 +120,13 @@ export const fetchLocationById = async (summitId, locationId, expand, accessToke if (expand) apiUrl.addQuery('expand', expand); - return fetch(apiUrl.toString(), { - method: 'GET', - cache: "no-store", - }).then(async (response) => { - if (response.status === 200) { - return await response.json(); - } - return null; - }); + const url = apiUrl.toString(); + const cacheKey = url; + + return fetchWithEtag(url, cacheKey); } /** - * * @param summitId * @param speakerId * @param accessToken @@ -122,20 +142,12 @@ export const fetchSpeakerById = async (summitId, speakerId, accessToken = null) } const apiUrlWithParams = SpeakersAPIRequest.build(apiUrl); + const cacheKey = apiUrlWithParams; - return fetch(apiUrlWithParams, { - method: 'GET', - cache: "no-store", - }).then(async (response) => { - if (response.status === 200) { - return await response.json(); - } - return null; - }); + return fetchWithEtag(apiUrlWithParams, cacheKey); } /** - * * @param summitId * @param accessToken * @returns {Promise} @@ -149,16 +161,9 @@ export const fetchSummitById = async (summitId, accessToken = null) => { } const apiUrlWithParams = SummitAPIRequest.build(apiUrl); + const cacheKey = apiUrlWithParams; - return fetch(apiUrlWithParams, { - method: 'GET', - cache: "no-store", - }).then(async (response) => { - if (response.status === 200) { - return await response.json(); - } - return null; - }); + return fetchWithEtag(apiUrlWithParams, cacheKey); } /** @@ -171,7 +176,7 @@ export const fetchTrackById = async (summitId, trackId, accessToken = null) => { let apiUrl = URI(`${process.env.GATSBY_SUMMIT_API_BASE_URL}/api/public/v1/summits/${summitId}/tracks/${trackId}`); const fields = [ - "id", "name", "code", "order", "parent_id", "color","text_color", + "id", "name", "code", "order", "parent_id", "color", "text_color", "subtracks.id", "subtracks.name", "subtracks.code", "subtracks.order", "subtracks.parent_id", "subtracks.color", "subtracks.text_color", ]; @@ -182,13 +187,8 @@ export const fetchTrackById = async (summitId, trackId, accessToken = null) => { apiUrl.addQuery('relations', relations.join(',')); apiUrl.addQuery('expand', expand.join(',')); - return fetch(apiUrl.toString(), { - method: 'GET', - cache: "no-store", - }).then(async (response) => { - if (response.status === 200) { - return await response.json(); - } - return null; - }); -} + const url = apiUrl.toString(); + const cacheKey = url; + + return fetchWithEtag(url, cacheKey); +} \ No newline at end of file From 25cf3bf83e8244aff8be0026ff65ace77572dc53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Fri, 6 Mar 2026 16:31:33 -0300 Subject: [PATCH 2/2] fix: change etagCache structure, adjust functions, clear cache on real time updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/actions/fetch-entities-actions.js | 68 +++++++++---------- .../activity_synch_strategy.js | 4 +- .../activity_type_synch_strategy.js | 4 +- .../sync_strategies/speaker_synch_strategy.js | 3 +- .../sync_strategies/summit_synch_strategy.js | 4 +- .../sync_strategies/track_synch_strategy.js | 3 +- .../venue_room_synch_strategy.js | 4 +- 7 files changed, 47 insertions(+), 43 deletions(-) diff --git a/src/actions/fetch-entities-actions.js b/src/actions/fetch-entities-actions.js index b298d80a..ab492064 100644 --- a/src/actions/fetch-entities-actions.js +++ b/src/actions/fetch-entities-actions.js @@ -3,22 +3,22 @@ import SummitAPIRequest from "../utils/build-json/SummitAPIRequest"; import EventAPIRequest from "../utils/build-json/EventsAPIRequest"; import SpeakersAPIRequest from "../utils/build-json/SpeakersAPIRequest"; -const etagCache = {}; +const etagCache = new Map(); +const MAX_CACHE_SIZE = 100; -const byLowerCase = toFind => value => toLowerCase(value) === toFind; -const toLowerCase = value => value.toLowerCase(); -const getKeys = headers => Object.keys(headers); - -const getHeaderCaseInsensitive = (headerName, headers = {}) => { - const key = getKeys(headers).find(byLowerCase(headerName)); - return key ? headers[key] : undefined; +export const clearEtagCacheForUrl = (urlPattern) => { + for (const key of etagCache.keys()) { + if (key.includes(urlPattern)) { + etagCache.delete(key); + } + } }; -const fetchWithEtag = async (url, cacheKey) => { +const fetchWithEtag = async (url) => { const headers = {}; - if (etagCache.hasOwnProperty(cacheKey)) { - const { etag } = etagCache[cacheKey]; + if (etagCache.has(url)) { + const { etag } = etagCache.get(url); if (etag) { headers['If-None-Match'] = etag; } @@ -30,20 +30,28 @@ const fetchWithEtag = async (url, cacheKey) => { headers, }); - if (res.status === 304 && etagCache.hasOwnProperty(cacheKey)) { - const { body } = etagCache[cacheKey]; + if (res.status === 304 && etagCache.has(url)) { + const { body } = etagCache.get(url); return body; } if (res.status === 200) { const data = await res.json(); - const responseETAG = getHeaderCaseInsensitive('etag', res.headers); + const responseETAG = res.headers.get('etag'); if (responseETAG) { - etagCache[cacheKey] = { etag: responseETAG, body: data }; + if (etagCache.size >= MAX_CACHE_SIZE) { + const oldest = etagCache.keys().next().value; + etagCache.delete(oldest); + } + etagCache.set(url, { etag: responseETAG, body: data }); } return data; } + if (!res.ok) { + console.error(`fetchWithEtag failed (${res.status}):`, url); + } + return null; }; @@ -62,9 +70,7 @@ export const fetchEventById = async (summitId, eventId, accessToken = null) => { } const apiUrlWithParams = EventAPIRequest.build(apiUrl); - const cacheKey = apiUrlWithParams; - - return fetchWithEtag(apiUrlWithParams, cacheKey); + return fetchWithEtag(apiUrlWithParams); } /** @@ -77,9 +83,7 @@ export const fetchStreamingInfoByEventId = async (summitId, eventId, accessToken const apiUrl = URI(`${process.env.GATSBY_SUMMIT_API_BASE_URL}/api/v1/summits/${summitId}/events/${eventId}/published/streaming-info`); apiUrl.addQuery('access_token', accessToken); const url = apiUrl.toString(); - const cacheKey = url; - - return fetchWithEtag(url, cacheKey); + return fetchWithEtag(url); } /** @@ -97,9 +101,7 @@ export const fetchEventTypeById = async (summitId, eventTypeId, accessToken = nu } const url = apiUrl.toString(); - const cacheKey = url; - - return fetchWithEtag(url, cacheKey); + return fetchWithEtag(url); } /** @@ -121,9 +123,7 @@ export const fetchLocationById = async (summitId, locationId, expand, accessToke apiUrl.addQuery('expand', expand); const url = apiUrl.toString(); - const cacheKey = url; - - return fetchWithEtag(url, cacheKey); + return fetchWithEtag(url); } /** @@ -142,9 +142,7 @@ export const fetchSpeakerById = async (summitId, speakerId, accessToken = null) } const apiUrlWithParams = SpeakersAPIRequest.build(apiUrl); - const cacheKey = apiUrlWithParams; - - return fetchWithEtag(apiUrlWithParams, cacheKey); + return fetchWithEtag(apiUrlWithParams); } /** @@ -161,9 +159,7 @@ export const fetchSummitById = async (summitId, accessToken = null) => { } const apiUrlWithParams = SummitAPIRequest.build(apiUrl); - const cacheKey = apiUrlWithParams; - - return fetchWithEtag(apiUrlWithParams, cacheKey); + return fetchWithEtag(apiUrlWithParams); } /** @@ -180,7 +176,7 @@ export const fetchTrackById = async (summitId, trackId, accessToken = null) => { "subtracks.id", "subtracks.name", "subtracks.code", "subtracks.order", "subtracks.parent_id", "subtracks.color", "subtracks.text_color", ]; - const relations = ['subtracks','subtracks.none']; + const relations = ['subtracks', 'subtracks.none']; const expand = ['subtracks'] apiUrl.addQuery('fields', fields.join(',')); @@ -188,7 +184,5 @@ export const fetchTrackById = async (summitId, trackId, accessToken = null) => { apiUrl.addQuery('expand', expand.join(',')); const url = apiUrl.toString(); - const cacheKey = url; - - return fetchWithEtag(url, cacheKey); + return fetchWithEtag(url); } \ No newline at end of file diff --git a/src/workers/sync_strategies/activity_synch_strategy.js b/src/workers/sync_strategies/activity_synch_strategy.js index 2bd649bc..7bd5f5e7 100644 --- a/src/workers/sync_strategies/activity_synch_strategy.js +++ b/src/workers/sync_strategies/activity_synch_strategy.js @@ -1,5 +1,5 @@ import AbstractSynchStrategy from "./abstract_synch_strategy"; -import {fetchEventById, fetchStreamingInfoByEventId} from "../../actions/fetch-entities-actions"; +import {clearEtagCacheForUrl, fetchEventById, fetchStreamingInfoByEventId} from "../../actions/fetch-entities-actions"; import {insertSorted, intCheck, rebuildIndex} from "../../utils/arrayUtils"; import { BUCKET_EVENTS_DATA_KEY, @@ -161,8 +161,10 @@ class ActivitySynchStrategy extends AbstractSynchStrategy{ switch (entity_operator) { case 'INSERT': case 'UPDATE':{ + clearEtagCacheForUrl(`/v1/summits/${this.summit.id}/events/${entity_id}/published`); let entity = await fetchEventById(this.summit.id, entity_id, this.accessToken); if(this.accessToken && this._shouldFetchStreamingInfo(this.currentLocation)) { + clearEtagCacheForUrl(`/v1/summits/${this.summit.id}/events/${entity_id}/published/streaming-info`) const streaming_info = await fetchStreamingInfoByEventId(this.summit.id, entity_id, this.accessToken); if(streaming_info) entity = {...entity, ...streaming_info}; } diff --git a/src/workers/sync_strategies/activity_type_synch_strategy.js b/src/workers/sync_strategies/activity_type_synch_strategy.js index 4a0f796b..ce2cc7a0 100644 --- a/src/workers/sync_strategies/activity_type_synch_strategy.js +++ b/src/workers/sync_strategies/activity_type_synch_strategy.js @@ -1,5 +1,5 @@ import AbstractSynchStrategy from "./abstract_synch_strategy"; -import {fetchEventTypeById} from "../../actions/fetch-entities-actions"; +import {clearEtagCacheForUrl, fetchEventTypeById} from "../../actions/fetch-entities-actions"; import { BUCKET_EVENTS_DATA_KEY, BUCKET_SUMMIT_DATA_KEY, @@ -17,6 +17,8 @@ class ActivityTypeSynchStrategy extends AbstractSynchStrategy{ const {entity_operator, entity_id} = payload; + clearEtagCacheForUrl(`/v1/summits/${this.summit.id}/event-types/${entity_id}`); + const entity = await fetchEventTypeById(this.summit.id, entity_id, this.accessToken); if (entity_operator === 'UPDATE') { diff --git a/src/workers/sync_strategies/speaker_synch_strategy.js b/src/workers/sync_strategies/speaker_synch_strategy.js index 2e843e29..12c104a3 100644 --- a/src/workers/sync_strategies/speaker_synch_strategy.js +++ b/src/workers/sync_strategies/speaker_synch_strategy.js @@ -1,5 +1,5 @@ import AbstractSynchStrategy from "./abstract_synch_strategy"; -import {fetchSpeakerById} from "../../actions/fetch-entities-actions"; +import {clearEtagCacheForUrl, fetchSpeakerById} from "../../actions/fetch-entities-actions"; import { BUCKET_SUMMIT_DATA_KEY, BUCKET_EVENTS_DATA_KEY, @@ -23,6 +23,7 @@ class SpeakerSynchStrategy extends AbstractSynchStrategy { const {entity_operator, entity_id} = payload; if (entity_operator === 'UPDATE') { + clearEtagCacheForUrl(`/v1/summits/${this.summit.id}/speakers/${entity_id}`); const entity = await fetchSpeakerById(this.summit.id, entity_id, this.accessToken); diff --git a/src/workers/sync_strategies/summit_synch_strategy.js b/src/workers/sync_strategies/summit_synch_strategy.js index a0e3471e..2deccfad 100644 --- a/src/workers/sync_strategies/summit_synch_strategy.js +++ b/src/workers/sync_strategies/summit_synch_strategy.js @@ -1,5 +1,5 @@ import AbstractSynchStrategy from "./abstract_synch_strategy"; -import {fetchSummitById} from "../../actions/fetch-entities-actions"; +import {clearEtagCacheForUrl, fetchSummitById} from "../../actions/fetch-entities-actions"; import { BUCKET_SUMMIT_DATA_KEY, saveFile @@ -17,6 +17,8 @@ class SummitSynchStrategy extends AbstractSynchStrategy { const {entity_operator} = payload; + clearEtagCacheForUrl(`/v1/summits/${this.summit.id}?`); + let entity = await fetchSummitById(this.summit.id, this.accessToken); let eventsData = [...this.allEvents]; diff --git a/src/workers/sync_strategies/track_synch_strategy.js b/src/workers/sync_strategies/track_synch_strategy.js index 71efcd78..abf65476 100644 --- a/src/workers/sync_strategies/track_synch_strategy.js +++ b/src/workers/sync_strategies/track_synch_strategy.js @@ -1,5 +1,5 @@ import AbstractSynchStrategy from "./abstract_synch_strategy"; -import {fetchTrackById} from "../../actions/fetch-entities-actions"; +import {clearEtagCacheForUrl, fetchTrackById} from "../../actions/fetch-entities-actions"; import { BUCKET_EVENTS_DATA_KEY, BUCKET_EVENTS_IDX_DATA_KEY, @@ -159,6 +159,7 @@ class TrackSynchStrategy extends AbstractSynchStrategy { switch (entity_operator) { case 'INSERT': case 'UPDATE':{ + clearEtagCacheForUrl(`/v1/summits/${this.summit.id}/tracks/${entity_id}`); const entity = await fetchTrackById(this.summit.id, entity_id, this.accessToken); if (!entity) return Promise.reject('TrackSynchStrategy::process entity not found.'); return this._handleUpsert(entity, payload); diff --git a/src/workers/sync_strategies/venue_room_synch_strategy.js b/src/workers/sync_strategies/venue_room_synch_strategy.js index 91f27b48..edb856aa 100644 --- a/src/workers/sync_strategies/venue_room_synch_strategy.js +++ b/src/workers/sync_strategies/venue_room_synch_strategy.js @@ -1,5 +1,5 @@ import AbstractSynchStrategy from "./abstract_synch_strategy"; -import { fetchLocationById } from "../../actions/fetch-entities-actions"; +import { clearEtagCacheForUrl, fetchLocationById } from "../../actions/fetch-entities-actions"; import { BUCKET_EVENTS_DATA_KEY, BUCKET_EVENTS_IDX_DATA_KEY, @@ -20,6 +20,8 @@ class VenueRoomSynchStrategy extends AbstractSynchStrategy{ const {entity_operator, entity_id} = payload; + clearEtagCacheForUrl(`/v1/summits/${this.summit.id}/locations/${entity_id}`); + const entity = await fetchLocationById(this.summit.id, entity_id, 'floor,venue' , this.accessToken); let eventsData = [...this.allEvents];