From 09627ce715905965b3cd099f76166c13b1ad0e2b Mon Sep 17 00:00:00 2001 From: Alexandr Garbuzov <186095128+alexandr-garbuzov@users.noreply.github.com> Date: Tue, 7 Oct 2025 00:13:21 +0300 Subject: [PATCH] refactor: move cache headers logic into reusable functions (#4533) * refactor: move cache headers logic into reusable functions * dev --------- Co-authored-by: Alexandr --- api/gist.js | 20 ++++++++------------ api/index.js | 20 ++++++++------------ api/pin.js | 20 ++++++++------------ api/top-langs.js | 20 ++++++++------------ api/wakatime.js | 22 ++++++++------------- src/common/cache.js | 36 ++++++++++++++++++++++++++++++++--- tests/api.test.js | 42 ++++++++++++++++++++--------------------- tests/gist.test.js | 2 +- tests/pin.test.js | 6 +++--- tests/top-langs.test.js | 6 +++--- 10 files changed, 100 insertions(+), 94 deletions(-) diff --git a/api/gist.js b/api/gist.js index d5928950..435c6b72 100644 --- a/api/gist.js +++ b/api/gist.js @@ -5,7 +5,11 @@ import { gistWhitelist } from "../src/common/envs.js"; import { isLocaleAvailable } from "../src/translations.js"; import { renderGistCard } from "../src/cards/gist.js"; import { fetchGist } from "../src/fetchers/gist.js"; -import { resolveCacheSeconds } from "../src/common/cache.js"; +import { + resolveCacheSeconds, + setCacheHeaders, + setErrorCacheHeaders, +} from "../src/common/cache.js"; export default async (req, res) => { const { @@ -57,16 +61,13 @@ export default async (req, res) => { try { const gistData = await fetchGist(id); const cacheSeconds = resolveCacheSeconds({ - requested: cache_seconds, + requested: parseInt(cache_seconds, 10), def: CONSTANTS.TWO_DAY, min: CONSTANTS.TWO_DAY, max: CONSTANTS.SIX_DAY, }); - res.setHeader( - "Cache-Control", - `max-age=${cacheSeconds}, s-maxage=${cacheSeconds}`, - ); + setCacheHeaders(res, cacheSeconds); return res.send( renderGistCard(gistData, { @@ -83,12 +84,7 @@ export default async (req, res) => { }), ); } catch (err) { - res.setHeader( - "Cache-Control", - `max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${ - CONSTANTS.ERROR_CACHE_SECONDS - }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, - ); // Use lower cache period for errors. + setErrorCacheHeaders(res); return res.send( renderError(err.message, err.secondaryMessage, { title_color, diff --git a/api/index.js b/api/index.js index 989e39a6..88ffd2db 100644 --- a/api/index.js +++ b/api/index.js @@ -2,7 +2,11 @@ import { renderStatsCard } from "../src/cards/stats.js"; import { blacklist } from "../src/common/blacklist.js"; -import { resolveCacheSeconds } from "../src/common/cache.js"; +import { + resolveCacheSeconds, + setCacheHeaders, + setErrorCacheHeaders, +} from "../src/common/cache.js"; import { whitelist } from "../src/common/envs.js"; import { CONSTANTS, @@ -104,16 +108,13 @@ export default async (req, res) => { parseInt(commits_year, 10), ); const cacheSeconds = resolveCacheSeconds({ - requested: cache_seconds, + requested: parseInt(cache_seconds, 10), def: CONSTANTS.CARD_CACHE_SECONDS, min: CONSTANTS.TWELVE_HOURS, max: CONSTANTS.TWO_DAY, }); - res.setHeader( - "Cache-Control", - `max-age=${cacheSeconds}, s-maxage=${cacheSeconds}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, - ); + setCacheHeaders(res, cacheSeconds); return res.send( renderStatsCard(stats, { @@ -144,12 +145,7 @@ export default async (req, res) => { }), ); } catch (err) { - res.setHeader( - "Cache-Control", - `max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${ - CONSTANTS.ERROR_CACHE_SECONDS - }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, - ); // Use lower cache period for errors. + setErrorCacheHeaders(res); return res.send( renderError(err.message, err.secondaryMessage, { title_color, diff --git a/api/pin.js b/api/pin.js index 10c52f1f..2907774c 100644 --- a/api/pin.js +++ b/api/pin.js @@ -2,7 +2,11 @@ import { renderRepoCard } from "../src/cards/repo.js"; import { blacklist } from "../src/common/blacklist.js"; -import { resolveCacheSeconds } from "../src/common/cache.js"; +import { + resolveCacheSeconds, + setCacheHeaders, + setErrorCacheHeaders, +} from "../src/common/cache.js"; import { whitelist } from "../src/common/envs.js"; import { CONSTANTS, parseBoolean, renderError } from "../src/common/utils.js"; import { fetchRepo } from "../src/fetchers/repo.js"; @@ -77,16 +81,13 @@ export default async (req, res) => { try { const repoData = await fetchRepo(username, repo); const cacheSeconds = resolveCacheSeconds({ - requested: cache_seconds, + requested: parseInt(cache_seconds, 10), def: CONSTANTS.PIN_CARD_CACHE_SECONDS, min: CONSTANTS.ONE_DAY, max: CONSTANTS.TEN_DAY, }); - res.setHeader( - "Cache-Control", - `max-age=${cacheSeconds}, s-maxage=${cacheSeconds}`, - ); + setCacheHeaders(res, cacheSeconds); return res.send( renderRepoCard(repoData, { @@ -104,12 +105,7 @@ export default async (req, res) => { }), ); } catch (err) { - res.setHeader( - "Cache-Control", - `max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${ - CONSTANTS.ERROR_CACHE_SECONDS - }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, - ); // Use lower cache period for errors. + setErrorCacheHeaders(res); return res.send( renderError(err.message, err.secondaryMessage, { title_color, diff --git a/api/top-langs.js b/api/top-langs.js index ce3443ba..9e7df07f 100644 --- a/api/top-langs.js +++ b/api/top-langs.js @@ -2,7 +2,11 @@ import { renderTopLanguages } from "../src/cards/top-languages.js"; import { blacklist } from "../src/common/blacklist.js"; -import { resolveCacheSeconds } from "../src/common/cache.js"; +import { + resolveCacheSeconds, + setCacheHeaders, + setErrorCacheHeaders, +} from "../src/common/cache.js"; import { whitelist } from "../src/common/envs.js"; import { CONSTANTS, @@ -106,16 +110,13 @@ export default async (req, res) => { count_weight, ); const cacheSeconds = resolveCacheSeconds({ - requested: cache_seconds, + requested: parseInt(cache_seconds, 10), def: CONSTANTS.TOP_LANGS_CACHE_SECONDS, min: CONSTANTS.TWO_DAY, max: CONSTANTS.TEN_DAY, }); - res.setHeader( - "Cache-Control", - `max-age=${cacheSeconds / 2}, s-maxage=${cacheSeconds}`, - ); + setCacheHeaders(res, cacheSeconds); return res.send( renderTopLanguages(topLangs, { @@ -139,12 +140,7 @@ export default async (req, res) => { }), ); } catch (err) { - res.setHeader( - "Cache-Control", - `max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${ - CONSTANTS.ERROR_CACHE_SECONDS - }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, - ); // Use lower cache period for errors. + setErrorCacheHeaders(res); return res.send( renderError(err.message, err.secondaryMessage, { title_color, diff --git a/api/wakatime.js b/api/wakatime.js index 5aba93ab..e3fedd5d 100644 --- a/api/wakatime.js +++ b/api/wakatime.js @@ -10,7 +10,11 @@ import { import { whitelist } from "../src/common/envs.js"; import { fetchWakatimeStats } from "../src/fetchers/wakatime.js"; import { isLocaleAvailable } from "../src/translations.js"; -import { resolveCacheSeconds } from "../src/common/cache.js"; +import { + resolveCacheSeconds, + setCacheHeaders, + setErrorCacheHeaders, +} from "../src/common/cache.js"; export default async (req, res) => { const { @@ -72,18 +76,13 @@ export default async (req, res) => { try { const stats = await fetchWakatimeStats({ username, api_domain }); const cacheSeconds = resolveCacheSeconds({ - requested: cache_seconds, + requested: parseInt(cache_seconds, 10), def: CONSTANTS.CARD_CACHE_SECONDS, min: CONSTANTS.SIX_HOURS, max: CONSTANTS.TWO_DAY, }); - res.setHeader( - "Cache-Control", - `max-age=${ - cacheSeconds / 2 - }, s-maxage=${cacheSeconds}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, - ); + setCacheHeaders(res, cacheSeconds); return res.send( renderWakatimeCard(stats, { @@ -109,12 +108,7 @@ export default async (req, res) => { }), ); } catch (err) { - res.setHeader( - "Cache-Control", - `max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${ - CONSTANTS.ERROR_CACHE_SECONDS - }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, - ); // Use lower cache period for errors. + setErrorCacheHeaders(res); return res.send( renderError(err.message, err.secondaryMessage, { title_color, diff --git a/src/common/cache.js b/src/common/cache.js index b5fd8ce7..013c16bc 100644 --- a/src/common/cache.js +++ b/src/common/cache.js @@ -1,4 +1,6 @@ -import { clampValue } from "./utils"; +// @ts-check + +import { clampValue, CONSTANTS } from "./utils.js"; /** * Resolves the cache seconds based on the requested, default, min, and max values. @@ -11,7 +13,7 @@ import { clampValue } from "./utils"; * @returns {number} The resolved cache seconds. */ const resolveCacheSeconds = ({ requested, def, min, max }) => { - let cacheSeconds = clampValue(parseInt(requested || def, 10), min, max); + let cacheSeconds = clampValue(isNaN(requested) ? def : requested, min, max); cacheSeconds = process.env.CACHE_SECONDS ? parseInt(process.env.CACHE_SECONDS, 10) || cacheSeconds @@ -20,4 +22,32 @@ const resolveCacheSeconds = ({ requested, def, min, max }) => { return cacheSeconds; }; -export { resolveCacheSeconds }; +/** + * Sets the Cache-Control headers on the response object. + * + * @param {Object} res The response object. + * @param {number} cacheSeconds The cache seconds to set in the headers. + */ +const setCacheHeaders = (res, cacheSeconds) => { + res.setHeader( + "Cache-Control", + `max-age=${cacheSeconds}, s-maxage=${cacheSeconds}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, + ); +}; + +/** + * Sets the Cache-Control headers for error responses on the response object. + * + * @param {Object} res The response object. + */ +const setErrorCacheHeaders = (res) => { + // Use lower cache period for errors. + res.setHeader( + "Cache-Control", + `max-age=${CONSTANTS.ERROR_CACHE_SECONDS}, ` + + `s-maxage=${CONSTANTS.ERROR_CACHE_SECONDS}, ` + + `stale-while-revalidate=${CONSTANTS.ONE_DAY}`, + ); +}; + +export { resolveCacheSeconds, setCacheHeaders, setErrorCacheHeaders }; diff --git a/tests/api.test.js b/tests/api.test.js index 44928887..e77775e9 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -179,9 +179,9 @@ describe("Test /api/", () => { ["Content-Type", "image/svg+xml"], [ "Cache-Control", - `max-age=${CONSTANTS.CARD_CACHE_SECONDS}, s-maxage=${ - CONSTANTS.CARD_CACHE_SECONDS - }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, + `max-age=${CONSTANTS.CARD_CACHE_SECONDS}, ` + + `s-maxage=${CONSTANTS.CARD_CACHE_SECONDS}, ` + + `stale-while-revalidate=${CONSTANTS.ONE_DAY}`, ], ]); }); @@ -195,11 +195,9 @@ describe("Test /api/", () => { ["Content-Type", "image/svg+xml"], [ "Cache-Control", - `max-age=${ - cache_seconds - }, s-maxage=${cache_seconds}, stale-while-revalidate=${ - CONSTANTS.ONE_DAY - }`, + `max-age=${cache_seconds}, ` + + `s-maxage=${cache_seconds}, ` + + `stale-while-revalidate=${CONSTANTS.ONE_DAY}`, ], ]); }); @@ -212,25 +210,25 @@ describe("Test /api/", () => { ["Content-Type", "image/svg+xml"], [ "Cache-Control", - `max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${ - CONSTANTS.ERROR_CACHE_SECONDS - }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, + `max-age=${CONSTANTS.ERROR_CACHE_SECONDS}, ` + + `s-maxage=${CONSTANTS.ERROR_CACHE_SECONDS}, ` + + `stale-while-revalidate=${CONSTANTS.ONE_DAY}`, ], ]); }); it("should set proper cache with clamped values", async () => { { - let { req, res } = faker({ cache_seconds: 200000 }, data_stats); + let { req, res } = faker({ cache_seconds: 200_000 }, data_stats); await api(req, res); expect(res.setHeader.mock.calls).toEqual([ ["Content-Type", "image/svg+xml"], [ "Cache-Control", - `max-age=${CONSTANTS.TWO_DAY}, s-maxage=${ - CONSTANTS.TWO_DAY - }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, + `max-age=${CONSTANTS.TWO_DAY}, ` + + `s-maxage=${CONSTANTS.TWO_DAY}, ` + + `stale-while-revalidate=${CONSTANTS.ONE_DAY}`, ], ]); } @@ -244,24 +242,24 @@ describe("Test /api/", () => { ["Content-Type", "image/svg+xml"], [ "Cache-Control", - `max-age=${CONSTANTS.ONE_DAY}, s-maxage=${ - CONSTANTS.ONE_DAY - }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, + `max-age=${CONSTANTS.TWELVE_HOURS}, ` + + `s-maxage=${CONSTANTS.TWELVE_HOURS}, ` + + `stale-while-revalidate=${CONSTANTS.ONE_DAY}`, ], ]); } { - let { req, res } = faker({ cache_seconds: -10000 }, data_stats); + let { req, res } = faker({ cache_seconds: -10_000 }, data_stats); await api(req, res); expect(res.setHeader.mock.calls).toEqual([ ["Content-Type", "image/svg+xml"], [ "Cache-Control", - `max-age=${CONSTANTS.TWELVE_HOURS}, s-maxage=${ - CONSTANTS.TWELVE_HOURS - }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, + `max-age=${CONSTANTS.TWELVE_HOURS}, ` + + `s-maxage=${CONSTANTS.TWELVE_HOURS}, ` + + `stale-while-revalidate=${CONSTANTS.ONE_DAY}`, ], ]); } diff --git a/tests/gist.test.js b/tests/gist.test.js index fdbf9cf5..ff336f1f 100644 --- a/tests/gist.test.js +++ b/tests/gist.test.js @@ -188,7 +188,7 @@ describe("Test /api/gist", () => { expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); expect(res.setHeader).toBeCalledWith( "Cache-Control", - `max-age=${CONSTANTS.TWO_DAY}, s-maxage=${CONSTANTS.TWO_DAY}`, + `max-age=${CONSTANTS.TWO_DAY}, s-maxage=${CONSTANTS.TWO_DAY}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, ); }); }); diff --git a/tests/pin.test.js b/tests/pin.test.js index 15a93b21..10324d44 100644 --- a/tests/pin.test.js +++ b/tests/pin.test.js @@ -224,9 +224,9 @@ describe("Test /api/pin", () => { expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); expect(res.setHeader).toBeCalledWith( "Cache-Control", - `max-age=${CONSTANTS.PIN_CARD_CACHE_SECONDS}, s-maxage=${ - CONSTANTS.PIN_CARD_CACHE_SECONDS - }`, + `max-age=${CONSTANTS.PIN_CARD_CACHE_SECONDS}, ` + + `s-maxage=${CONSTANTS.PIN_CARD_CACHE_SECONDS}, ` + + `stale-while-revalidate=${CONSTANTS.ONE_DAY}`, ); }); }); diff --git a/tests/top-langs.test.js b/tests/top-langs.test.js index c4b0fa97..0a51d4e3 100644 --- a/tests/top-langs.test.js +++ b/tests/top-langs.test.js @@ -230,9 +230,9 @@ describe("Test /api/top-langs", () => { expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); expect(res.setHeader).toBeCalledWith( "Cache-Control", - `max-age=${CONSTANTS.TOP_LANGS_CACHE_SECONDS / 2}, s-maxage=${ - CONSTANTS.TOP_LANGS_CACHE_SECONDS - }`, + `max-age=${CONSTANTS.TOP_LANGS_CACHE_SECONDS}, ` + + `s-maxage=${CONSTANTS.TOP_LANGS_CACHE_SECONDS}, ` + + `stale-while-revalidate=${CONSTANTS.ONE_DAY}`, ); }); });