Organized caching + groundwork for page caching

This commit is contained in:
DrMint 2024-06-27 22:04:08 +02:00
parent d9ef48d811
commit 2cacccae86
23 changed files with 392 additions and 351 deletions

View File

@ -10,7 +10,6 @@
## Short term
- [Bugs] On android Chrome, the setting button in the header flashes for a few ms when the page is loading
- [Feat] [caching] Use getURLs for precaching + precache everything
- [Bugs] Make sure uploads name are slug-like and with an extension.
- [Bugs] Nyupun can't upload subtitles files
- [Bugs] https://v3.accords-library.com/en/collectibles/dod-original-soundtrack/scans obi is way too big

58
src/cache/contextCache.ts vendored Normal file
View File

@ -0,0 +1,58 @@
import type {
EndpointWebsiteConfig,
EndpointWording,
Language,
} from "src/shared/payload/payload-sdk";
import { getLogger } from "src/utils/logger";
import { payload } from "src/utils/payload";
class ContextCache {
private initialized = false;
private logger = getLogger("[ContextCache]");
locales: Language[] = [];
currencies: string[] = [];
wordings: EndpointWording[] = [];
config: EndpointWebsiteConfig = {
home: { folders: [] },
timeline: { breaks: [], eras: [], eventCount: 0 },
};
async init() {
if (this.initialized) return;
await this.refreshAll();
this.initialized = true;
this.logger.log("Init complete");
}
private async refreshAll() {
await this.refreshCurrencies();
await this.refreshLocales();
await this.refreshWebsiteConfig();
await this.refreshWordings();
}
async refreshWordings() {
this.wordings = await payload.getWordings();
this.logger.log("Wordings refreshed");
}
async refreshCurrencies() {
contextCache.currencies = (await payload.getCurrencies()).map(({ id }) => id);
this.logger.log("Currencies refreshed");
}
async refreshLocales() {
contextCache.locales = await payload.getLanguages();
this.logger.log("Locales refreshed");
}
async refreshWebsiteConfig() {
contextCache.config = await payload.getConfig();
this.logger.log("WebsiteConfig refreshed");
}
}
const contextCache = new ContextCache();
await contextCache.init();
export { contextCache };

84
src/cache/dataCache.ts vendored Normal file
View File

@ -0,0 +1,84 @@
import { getLogger } from "src/utils/logger";
import { payload } from "src/utils/payload";
class DataCache {
private readonly logger = getLogger("[DataCache]");
private initialized = false;
private readonly responseCache = new Map<string, any>();
private readonly idsCacheMap = new Map<string, Set<string>>();
async init() {
if (this.initialized) return;
if (import.meta.env.ENABLE_PRECACHING === "true") {
await this.precache();
}
this.initialized = true;
}
private async precache() {
const { urls } = await payload.getAllSdkUrls();
for (const url of urls) {
try {
await payload.request(url);
} catch {
this.logger.warn("Precaching failed for url", url);
}
}
this.logger.log("Precaching completed!", this.responseCache.size, "responses cached");
}
get(url: string) {
const cachedResponse = this.responseCache.get(url);
if (cachedResponse) {
this.logger.log("Retrieved cached response for", url);
return structuredClone(cachedResponse);
}
}
set(url: string, response: any) {
const stringData = JSON.stringify(response);
const regex = /[a-f0-9]{24}/g;
const ids = [...stringData.matchAll(regex)].map((match) => match[0]);
const uniqueIds = [...new Set(ids)];
uniqueIds.forEach((id) => {
const current = this.idsCacheMap.get(id);
if (current) {
current.add(url);
} else {
this.idsCacheMap.set(id, new Set([url]));
}
});
this.responseCache.set(url, response);
this.logger.log("Cached response for", url);
}
async invalidate(ids: string[], urls: string[]) {
const urlsToInvalidate = new Set<string>(urls);
ids.forEach((id) => {
const urlsForThisId = this.idsCacheMap.get(id);
if (!urlsForThisId) return;
this.idsCacheMap.delete(id);
[...urlsForThisId].forEach((url) => urlsToInvalidate.add(url));
});
for (const url of urlsToInvalidate) {
this.responseCache.delete(url);
this.logger.log("Invalidated cache for", url);
try {
await payload.request(url);
} catch (e) {
this.logger.log("Revalidation fails for", url);
}
}
this.logger.log("There are currently", this.responseCache.size, "responses in cache.");
}
}
export const dataCache = new DataCache();

View File

@ -7,7 +7,7 @@ import type {
EndpointPayloadImage,
EndpointVideo,
} from "src/shared/payload/payload-sdk";
import { cache } from "src/utils/payload";
import { contextCache } from "src/cache/contextCache";
interface Props {
openGraph?:
@ -28,7 +28,9 @@ const { openGraph = {} } = Astro.props;
const { description = t("global.meta.description"), audio, video } = openGraph;
const thumbnail =
openGraph.thumbnail?.openGraph ?? openGraph.thumbnail ?? cache.config.defaultOpenGraphImage;
openGraph.thumbnail?.openGraph ??
openGraph.thumbnail ??
contextCache.config.defaultOpenGraphImage;
const title = openGraph.title
? `${openGraph.title} ${t("global.siteName")}`
@ -38,8 +40,6 @@ const userAgent = Astro.request.headers.get("user-agent") ?? "";
const parser = new UAParser(userAgent);
const isIOS = parser.getOS().name === "iOS";
const { currentTheme } = Astro.locals;
/* Keep that separator here or else it breaks the HTML
----------------------------------------------- HTML -------------------------------------------- */
---
@ -47,9 +47,7 @@ const { currentTheme } = Astro.locals;
<html
lang={currentLocale}
class:list={{
"manual-theme": currentTheme !== "auto",
"light-theme": currentTheme === "light",
"dark-theme": currentTheme === "dark",
POST_PROCESS_HTML_CLASS: true,
"texture-dots": !isIOS,
"font-m": true,
"debug-lang": false,
@ -310,7 +308,7 @@ const { currentTheme } = Astro.locals;
color: var(--color-base-1000);
}
html:not(.manual-theme) {
html:not(.light-theme, .dark-theme) {
@media (prefers-color-scheme: light) {
& .when-dark-theme {
display: none !important;

View File

@ -2,7 +2,7 @@
import Button from "components/Button.astro";
import Tooltip from "components/Tooltip.astro";
import { getI18n } from "src/i18n/i18n";
import { cache } from "src/utils/payload";
import { contextCache } from "src/cache/contextCache";
import { formatCurrency } from "src/utils/currencies";
interface Props {
@ -21,14 +21,13 @@ const { currentCurrency } = Astro.locals;
<Tooltip trigger="click" {...otherProps.class ? otherProps : {}}>
<div id="content" slot="tooltip-content">
{
cache.currencies.map((id) => (
contextCache.currencies.map((id) => (
<a
class:list={{
current: currentCurrency === id,
"pressable-link": true,
}}
href={`?action-currency=${id}`}
data-astro-prefetch="tap">
href={`?action-currency=${id}`}>
{`${id} (${formatCurrency(id)})`}
</a>
))

View File

@ -2,7 +2,7 @@
import Button from "components/Button.astro";
import Tooltip from "components/Tooltip.astro";
import { getI18n } from "src/i18n/i18n";
import { cache } from "src/utils/payload";
import { contextCache } from "src/cache/contextCache";
import { formatLocale } from "src/utils/format";
interface Props {
@ -21,11 +21,10 @@ const { t } = await getI18n(currentLocale);
<Tooltip trigger="click" {...otherProps.class ? otherProps : {}}>
<div id="content" slot="tooltip-content">
{
cache.locales.map(({ id }) => (
contextCache.locales.map(({ id }) => (
<a
class:list={{ current: currentLocale === id, "pressable-link": true }}
href={`?action-lang=${id}`}
data-astro-prefetch="tap">
href={`?action-lang=${id}`}>
{formatLocale(id)}
</a>
))

View File

@ -3,7 +3,7 @@ import Button from "components/Button.astro";
import Tooltip from "components/Tooltip.astro";
import { getI18n } from "src/i18n/i18n";
const { currentLocale, currentTheme } = Astro.locals;
const { currentLocale } = Astro.locals;
const { t } = await getI18n(currentLocale);
---
@ -11,19 +11,13 @@ const { t } = await getI18n(currentLocale);
<Tooltip trigger="click">
<div id="content" slot="tooltip-content">
<a
class:list={{ current: currentTheme === "dark", "pressable-link": true }}
href="?action-theme=dark">
<a class="pressable-link underline-when-dark" href="?action-theme=dark">
{t("global.theme.dark")}
</a>
<a
class:list={{ current: currentTheme === "auto", "pressable-link": true }}
href="?action-theme=auto">
<a class="pressable-link underline-when-auto" href="?action-theme=auto">
{t("global.theme.auto")}
</a>
<a
class:list={{ current: currentTheme === "light", "pressable-link": true }}
href="?action-theme=light">
<a class="pressable-link underline-when-light" href="?action-theme=light">
{t("global.theme.light")}
</a>
</div>
@ -45,10 +39,12 @@ const { t } = await getI18n(currentLocale);
#content {
display: grid;
gap: 0.5em;
}
& > .current {
color: var(--color-base-750);
text-decoration: underline 0.08em var(--color-base-650);
}
:global(html.light-theme) a.underline-when-light,
:global(html.dark-theme) a.underline-when-dark,
:global(html:not(.light-theme, .dark-theme)) a.underline-when-auto {
color: var(--color-base-750);
text-decoration: underline 0.08em var(--color-base-650);
}
</style>

1
src/env.d.ts vendored
View File

@ -4,7 +4,6 @@
declare namespace App {
interface Locals {
currentLocale: string;
currentTheme: "dark" | "auto" | "light";
currentCurrency: string;
notFound: boolean;
}

View File

@ -1,6 +1,6 @@
import type { WordingKey } from "src/i18n/wordings-keys";
import type { ChronologyEvent, EndpointSource } from "src/shared/payload/payload-sdk";
import { cache } from "src/utils/payload";
import { contextCache } from "src/cache/contextCache";
import { capitalize, formatInlineTitle } from "src/utils/format";
export const defaultLocale = "en";
@ -113,7 +113,7 @@ export const getI18n = async (locale: string) => {
options[0]!; // We will consider that there will always be at least one option.
const t = (key: WordingKey, values: Record<string, any> = {}): string => {
const wording = cache.wordings.find(({ name }) => name === key);
const wording = contextCache.wordings.find(({ name }) => name === key);
const fallbackString = `«${key}»`;
if (!wording) {

View File

@ -1,10 +1,10 @@
import { defineMiddleware, sequence } from "astro:middleware";
import { cache } from "src/utils/payload";
import acceptLanguage from "accept-language";
import type { AstroCookies } from "astro";
import { z } from "astro:content";
import { trackRequest, trackEvent } from "src/shared/analytics/analytics";
import { defaultLocale } from "src/i18n/i18n";
import { contextCache } from "src/cache/contextCache";
const ninetyDaysInSeconds = 60 * 60 * 24 * 90;
@ -141,20 +141,22 @@ const refreshCookiesMaxAge = defineMiddleware(async ({ cookies }, next) => {
return response;
});
const addContentLanguageResponseHeader = defineMiddleware(async ({ url }, next) => {
const addCommonHeaders = defineMiddleware(async ({ url }, next) => {
const currentLocale = getCurrentLocale(url.pathname);
const response = await next();
if (response.status === 200 && currentLocale) {
if (response.ok && currentLocale) {
response.headers.set("Content-Language", currentLocale);
}
response.headers.set("Vary", "Cookie");
return response;
});
const provideLocalsToRequest = defineMiddleware(async ({ url, locals, cookies }, next) => {
locals.currentLocale = getCurrentLocale(url.pathname) ?? "en";
locals.currentCurrency = getCookieCurrency(cookies) ?? "USD";
locals.currentTheme = getCookieTheme(cookies) ?? "auto";
return next();
});
@ -165,19 +167,40 @@ const analytics = defineMiddleware(async (context, next) => {
return response;
});
const postProcess = defineMiddleware(async ({ cookies }, next) => {
const response = await next();
if (!response.ok) {
return response;
}
let html = await response.text();
const currentTheme = getCookieTheme(cookies) ?? "auto";
html = html.replace(
"POST_PROCESS_HTML_CLASS",
currentTheme === "dark" ? "dark-theme" : currentTheme === "light" ? "light-theme" : ""
);
return new Response(html, response);
});
export const onRequest = sequence(
addContentLanguageResponseHeader,
// Possible redirect
handleActionsSearchParams,
refreshCookiesMaxAge,
localeNegotiator,
addCommonHeaders,
// Get a response
analytics,
refreshCookiesMaxAge,
provideLocalsToRequest,
analytics
postProcess
);
/* LOCALE */
const getCurrentLocale = (pathname: string): string | undefined => {
for (const locale of cache.locales) {
for (const locale of contextCache.locales) {
if (pathname.split("/")[1] === locale.id) {
return locale.id;
}
@ -189,7 +212,7 @@ const getBestAcceptedLanguage = (request: Request): string | undefined => {
const header = request.headers.get("Accept-Language");
if (!header) return;
acceptLanguage.languages(cache.locales.map(({ id }) => id));
acceptLanguage.languages(contextCache.locales.map(({ id }) => id));
return acceptLanguage.get(request.headers.get("Accept-Language")) ?? undefined;
};
@ -220,10 +243,12 @@ export const getCookieTheme = (cookies: AstroCookies): z.infer<typeof themeSchem
};
export const isValidCurrency = (currency: string | null | undefined): currency is string =>
currency !== null && currency != undefined && cache.currencies.includes(currency);
currency !== null && currency != undefined && contextCache.currencies.includes(currency);
export const isValidLocale = (locale: string | null | undefined): locale is string =>
locale !== null && locale != undefined && cache.locales.map(({ id }) => id).includes(locale);
locale !== null &&
locale != undefined &&
contextCache.locales.map(({ id }) => id).includes(locale);
export const isValidTheme = (
theme: string | null | undefined

View File

@ -1,7 +1,7 @@
---
import LibraryCard from "./LibraryCard.astro";
import { getI18n } from "src/i18n/i18n";
import { cache } from "src/utils/payload";
import { contextCache } from "src/cache/contextCache";
const { getLocalizedUrl, getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
---
@ -9,7 +9,7 @@ const { getLocalizedUrl, getLocalizedMatch } = await getI18n(Astro.locals.curren
{/* ------------------------------------------- HTML ------------------------------------------- */}
{
cache.config.home.folders.map(({ slug, translations, darkThumbnail, lightThumbnail }) => (
contextCache.config.home.folders.map(({ slug, translations, darkThumbnail, lightThumbnail }) => (
<LibraryCard
img={
darkThumbnail && lightThumbnail ? { dark: darkThumbnail, light: lightThumbnail } : undefined

View File

@ -1,12 +1,7 @@
import type { APIRoute } from "astro";
import { contextCache } from "src/cache/contextCache";
import { dataCache } from "src/cache/dataCache";
import { Collections, type AfterOperationWebHookMessage } from "src/shared/payload/payload-sdk";
import {
invalidateDataCache,
refreshCurrencies,
refreshLocales,
refreshWebsiteConfig,
refreshWordings,
} from "src/utils/payload";
export const POST: APIRoute = async ({ request }) => {
const auth = request.headers.get("Authorization");
@ -30,23 +25,23 @@ const handleWebHookMessage = async ({
urls,
id,
}: AfterOperationWebHookMessage) => {
await invalidateDataCache([...(id ? [id] : []), ...addedDependantIds], urls);
await dataCache.invalidate([...(id ? [id] : []), ...addedDependantIds], urls);
switch (collection) {
case Collections.Wordings:
await refreshWordings();
await contextCache.refreshWordings();
break;
case Collections.Currencies:
await refreshCurrencies();
await contextCache.refreshCurrencies();
break;
case Collections.Languages:
await refreshLocales();
await contextCache.refreshLocales();
break;
case Collections.WebsiteConfig:
await refreshWebsiteConfig();
await contextCache.refreshWebsiteConfig();
break;
}
};

View File

@ -1,7 +1,7 @@
import type { APIRoute } from "astro";
import { initPayload } from "src/utils/payload";
import { dataCache } from "src/cache/dataCache";
export const GET: APIRoute = async () => {
await initPayload();
await dataCache.init();
return new Response(null, { status: 200, statusText: "Ok" });
};

View File

@ -9,7 +9,7 @@ interface Props {
const { baseColors, theme } = Astro.props;
---
<div id="container" class:list={[theme, "manual-theme"]}>
<div id="container" class:list={theme}>
<h4 class="font-xl">Base colors</h4>
<div class="colors">
{

View File

@ -3,7 +3,7 @@ import Button from "components/Button.astro";
import LibraryGrid from "./_components/LibraryGrid.astro";
import LinkCard from "./_components/LinkCard.astro";
import { getI18n } from "src/i18n/i18n";
import { cache } from "src/utils/payload";
import { contextCache } from "src/cache/contextCache";
import AppLayout from "components/AppLayout/AppLayout.astro";
import HomeTitle from "./_components/HomeTitle.astro";
@ -14,7 +14,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
<AppLayout
openGraph={{ title: t("home.title") }}
backgroundImage={cache.config.home.backgroundImage}
backgroundImage={contextCache.config.home.backgroundImage}
hideFooterLinks
hideHomeButton
class="app">
@ -77,8 +77,8 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
icon="material-symbols:calendar-month"
title={t("footer.links.timeline.title")}
subtitle={t("footer.links.timeline.subtitle", {
eraCount: cache.config.timeline.eras.length,
eventCount: cache.config.timeline.eventCount,
eraCount: contextCache.config.timeline.eras.length,
eventCount: contextCache.config.timeline.eventCount,
})}
href={getLocalizedUrl("/timeline")}
/>

View File

@ -2,11 +2,11 @@
import AppLayout from "components/AppLayout/AppLayout.astro";
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
import { getI18n } from "src/i18n/i18n";
import { cache } from "src/utils/payload";
import { contextCache } from "src/cache/contextCache";
import { formatCurrency } from "src/utils/currencies";
import { formatLocale } from "src/utils/format";
const { currentLocale, currentTheme, currentCurrency } = Astro.locals;
const { currentLocale, currentCurrency } = Astro.locals;
const { t } = await getI18n(currentLocale);
---
@ -20,11 +20,10 @@ const { t } = await getI18n(currentLocale);
<h2>{t("settings.language.title")}</h2>
<p>{t("settings.language.description")}</p><br />
{
cache.locales.map(({ id }) => (
contextCache.locales.map(({ id }) => (
<a
class:list={{ current: currentLocale === id, "pressable-link": true }}
href={`?action-lang=${id}`}
data-astro-prefetch="tap">
href={`?action-lang=${id}`}>
{formatLocale(id)}
</a>
))
@ -34,22 +33,13 @@ const { t } = await getI18n(currentLocale);
<div class="section">
<h2>{t("settings.theme.title")}</h2>
<p>{t("settings.theme.description")}</p><br />
<a
class:list={{ current: currentTheme === "dark", "pressable-link": true }}
href="?action-theme=dark"
data-astro-prefetch="tap">
<a class="pressable-link underline-when-dark" href="?action-theme=dark">
{t("global.theme.dark")}
</a>
<a
class:list={{ current: currentTheme === "auto", "pressable-link": true }}
href="?action-theme=auto"
data-astro-prefetch="tap">
<a class="pressable-link underline-when-auto" href="?action-theme=auto">
{t("global.theme.auto")}
</a>
<a
class:list={{ current: currentTheme === "light", "pressable-link": true }}
href="?action-theme=light"
data-astro-prefetch="tap">
<a class="pressable-link underline-when-light" href="?action-theme=light">
{t("global.theme.light")}
</a>
</div>
@ -58,11 +48,10 @@ const { t } = await getI18n(currentLocale);
<h2>{t("settings.currency.title")}</h2>
<p>{t("settings.currency.description")}</p><br />
{
cache.currencies.map((id) => (
contextCache.currencies.map((id) => (
<a
class:list={{ current: currentCurrency === id, "pressable-link": true }}
href={`?action-currency=${id}`}
data-astro-prefetch="tap">
href={`?action-currency=${id}`}>
{`${id} (${formatCurrency(id)})`}
</a>
))
@ -86,6 +75,13 @@ const { t } = await getI18n(currentLocale);
}
}
:global(html.light-theme) a.underline-when-light,
:global(html.dark-theme) a.underline-when-dark,
:global(html:not(.light-theme, .dark-theme)) a.underline-when-auto {
color: var(--color-base-750);
text-decoration: underline 0.08em var(--color-base-650);
}
#main {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));

View File

@ -2,7 +2,7 @@
import type { EndpointChronologyEvent } from "src/shared/payload/payload-sdk";
import TimelineEvent from "./TimelineEvent.astro";
import { getI18n } from "src/i18n/i18n";
import { cache } from "src/utils/payload";
import { contextCache } from "src/cache/contextCache";
interface Props {
year: number;
@ -25,7 +25,7 @@ if (year === 856) {
{/* ------------------------------------------- HTML ------------------------------------------- */}
{cache.config.timeline.breaks.includes(year) && <hr id={`hr-${year}`} />}
{contextCache.config.timeline.breaks.includes(year) && <hr id={`hr-${year}`} />}
<div>
<h2 class="font-2xl" class:list={{ multiple }} id={year.toString()}>

View File

@ -6,7 +6,7 @@ import AppLayout from "components/AppLayout/AppLayout.astro";
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
import Card from "components/Card.astro";
import { getI18n } from "src/i18n/i18n";
import { cache } from "src/utils/payload";
import { contextCache } from "src/cache/contextCache";
import type { WordingKey } from "src/i18n/wordings-keys";
const events = await payload.getChronologyEvents();
@ -16,7 +16,7 @@ const { getLocalizedUrl, t, formatTimelineDate } = await getI18n(Astro.locals.cu
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppLayout backgroundImage={cache.config.timeline.backgroundImage} class="app">
<AppLayout backgroundImage={contextCache.config.timeline.backgroundImage} class="app">
<AppLayoutTitle title={t("timeline.title")} />
<p class="prose" set:html={t("timeline.description")} />
@ -54,7 +54,7 @@ const { getLocalizedUrl, t, formatTimelineDate } = await getI18n(Astro.locals.cu
<h3>{t("timeline.jumpTo")}</h3>
{
cache.config.timeline.eras.map(({ name, startingYear, endingYear }) => (
contextCache.config.timeline.eras.map(({ name, startingYear, endingYear }) => (
<p
set:html={t(name as WordingKey, {
start: `<a href="#${startingYear}">${formatTimelineDate({ year: startingYear })}</a>`,

View File

@ -1,7 +1,7 @@
import { cache } from "src/utils/payload";
import { contextCache } from "src/cache/contextCache";
const getUnlocalizedPathname = (pathname: string): string => {
for (const locale of cache.locales) {
for (const locale of contextCache.locales) {
if (pathname.startsWith(`/${locale.id}`)) {
return pathname.substring(`/${locale.id}`.length) || "/";
}
@ -30,7 +30,6 @@ export const trackRequest = (request: Request, { clientAddress, locals }: TrackR
attributes: {
locale: locals.currentLocale,
currency: locals.currentCurrency,
theme: locals.currentTheme,
},
},
request: {

View File

@ -1,177 +1,177 @@
{
"disclaimer": "Usage subject to terms: https://openexchangerates.org/terms",
"license": "https://openexchangerates.org/license",
"timestamp": 1719374401,
"timestamp": 1719518400,
"base": "USD",
"rates": {
"AED": 3.673,
"AFN": 70.737389,
"ALL": 93.591883,
"AMD": 388.464511,
"ANG": 1.804398,
"AOA": 855.209667,
"ARS": 909.2076,
"AUD": 1.498775,
"AFN": 70,
"ALL": 93.656243,
"AMD": 388.12,
"ANG": 1.803057,
"AOA": 853.629,
"ARS": 911.0001,
"AUD": 1.504506,
"AWG": 1.8025,
"AZN": 1.7,
"BAM": 1.825905,
"BAM": 1.827765,
"BBD": 2,
"BDT": 117.631657,
"BGN": 1.8261,
"BHD": 0.376925,
"BIF": 2879.142593,
"BDT": 117.53659,
"BGN": 1.826865,
"BHD": 0.376928,
"BIF": 2882.5,
"BMD": 1,
"BND": 1.354432,
"BOB": 6.917863,
"BRL": 5.452199,
"BND": 1.357361,
"BOB": 6.912666,
"BRL": 5.5062,
"BSD": 1,
"BTC": 0.000016147327,
"BTN": 83.525391,
"BWP": 13.566127,
"BYN": 3.276229,
"BZD": 2.017943,
"CAD": 1.365775,
"CDF": 2845.344278,
"CHF": 0.895078,
"CLF": 0.034108,
"CLP": 943.396226,
"CNH": 7.292088,
"CNY": 7.2661,
"COP": 4092.688822,
"CRC": 524.018922,
"BTC": 0.000016286712,
"BTN": 83.510338,
"BWP": 13.649322,
"BYN": 3.274029,
"BZD": 2.016506,
"CAD": 1.36937,
"CDF": 2860,
"CHF": 0.898769,
"CLF": 0.034588,
"CLP": 954.24,
"CNH": 7.303225,
"CNY": 7.2683,
"COP": 4147.064273,
"CRC": 523.036765,
"CUC": 1,
"CUP": 25.75,
"CVE": 102.941746,
"CZK": 23.187699,
"DJF": 178.249371,
"DKK": 6.9642,
"DOP": 59.114309,
"DZD": 134.5516,
"EGP": 48.377851,
"CVE": 103.063904,
"CZK": 23.432,
"DJF": 177.5,
"DKK": 6.969109,
"DOP": 59.2,
"DZD": 134.481574,
"EGP": 48.0279,
"ERN": 15,
"ETB": 57.353303,
"EUR": 0.933624,
"FJD": 2.2362,
"FKP": 0.788219,
"GBP": 0.788219,
"GEL": 2.81,
"GGP": 0.788219,
"GHS": 15.216498,
"GIP": 0.788219,
"GMD": 67.75,
"GNF": 8617.038799,
"GTQ": 7.774894,
"GYD": 209.365764,
"HKD": 7.809496,
"HNL": 24.774423,
"HRK": 7.034221,
"HTG": 132.791853,
"HUF": 369.47,
"IDR": 16421.779805,
"ILS": 3.74785,
"IMP": 0.788219,
"INR": 83.447347,
"IQD": 1311.401658,
"IRR": 42087.5,
"ISK": 139.21,
"JEP": 0.788219,
"JMD": 156.444628,
"ETB": 57.750301,
"EUR": 0.934324,
"FJD": 2.24125,
"FKP": 0.791234,
"GBP": 0.791234,
"GEL": 2.8,
"GGP": 0.791234,
"GHS": 15.25,
"GIP": 0.791234,
"GMD": 67.775,
"GNF": 8595,
"GTQ": 7.773841,
"GYD": 209.310316,
"HKD": 7.808725,
"HNL": 24.762821,
"HRK": 7.039709,
"HTG": 132.614267,
"HUF": 370.35232,
"IDR": 16384.008966,
"ILS": 3.75783,
"IMP": 0.791234,
"INR": 83.466142,
"IQD": 1310.629281,
"IRR": 42100,
"ISK": 139.14,
"JEP": 0.791234,
"JMD": 156.065666,
"JOD": 0.7087,
"JPY": 159.8375,
"KES": 129.646953,
"KGS": 86.5868,
"KHR": 4119.876268,
"KMF": 460.124977,
"JPY": 160.819,
"KES": 129,
"KGS": 86.45,
"KHR": 4116,
"KMF": 460.04988,
"KPW": 900,
"KRW": 1390.097786,
"KWD": 0.306622,
"KYD": 0.834247,
"KZT": 467.84526,
"LAK": 22038.352227,
"LBP": 89647.700512,
"LKR": 305.644882,
"LRD": 194.349995,
"LSL": 18.152071,
"LYD": 4.855,
"MAD": 9.950426,
"MDL": 17.920244,
"MGA": 4473.169164,
"MKD": 57.439335,
"KRW": 1387.206613,
"KWD": 0.306775,
"KYD": 0.833746,
"KZT": 466.751615,
"LAK": 22077.44273,
"LBP": 89576.348385,
"LKR": 306.055904,
"LRD": 194.492735,
"LSL": 18.359053,
"LYD": 4.87349,
"MAD": 9.939151,
"MDL": 17.849263,
"MGA": 4476.966706,
"MKD": 57.476359,
"MMK": 2481.91,
"MNT": 3450,
"MOP": 8.051048,
"MRU": 39.352282,
"MUR": 46.929999,
"MOP": 8.047221,
"MRU": 39.452362,
"MUR": 46.899999,
"MVR": 15.405,
"MWK": 1735.964257,
"MXN": 18.104717,
"MYR": 4.71,
"MWK": 1734.657401,
"MXN": 18.41087,
"MYR": 4.7195,
"MZN": 63.850001,
"NAD": 18.152071,
"NGN": 1518.12,
"NIO": 36.848246,
"NOK": 10.607752,
"NPR": 133.640175,
"NZD": 1.635653,
"OMR": 0.384952,
"NAD": 18.359053,
"NGN": 1515.9,
"NIO": 36.825702,
"NOK": 10.666237,
"NPR": 133.611854,
"NZD": 1.643791,
"OMR": 0.384955,
"PAB": 1,
"PEN": 3.815747,
"PGK": 3.906082,
"PHP": 58.837749,
"PKR": 278.808073,
"PLN": 4.011559,
"PYG": 7549.795422,
"QAR": 3.651064,
"RON": 4.644,
"RSD": 109.292759,
"RUB": 87.502462,
"RWF": 1326.548635,
"SAR": 3.751926,
"PEN": 3.823237,
"PGK": 3.904308,
"PHP": 58.639498,
"PKR": 278.503056,
"PLN": 4.029254,
"PYG": 7540.098869,
"QAR": 3.649042,
"RON": 4.6507,
"RSD": 109.379523,
"RUB": 84.998456,
"RWF": 1306.142519,
"SAR": 3.751634,
"SBD": 8.43942,
"SCR": 13.861839,
"SCR": 13.796937,
"SDG": 601,
"SEK": 10.509533,
"SGD": 1.354893,
"SHP": 0.788219,
"SEK": 10.62955,
"SGD": 1.358205,
"SHP": 0.791234,
"SLL": 20969.5,
"SOS": 572.148966,
"SRD": 31.0905,
"SOS": 571.751991,
"SRD": 30.623,
"SSP": 130.26,
"STD": 22281.8,
"STN": 22.872934,
"SVC": 8.759785,
"STN": 22.899229,
"SVC": 8.754335,
"SYP": 2512.53,
"SZL": 18.159157,
"THB": 36.7755,
"TJS": 10.696778,
"SZL": 18.178413,
"THB": 36.7685,
"TJS": 10.65474,
"TMT": 3.51,
"TND": 3.133858,
"TOP": 2.359259,
"TRY": 33.0006,
"TTD": 6.801133,
"TWD": 32.533,
"TZS": 2638.159,
"UAH": 40.666635,
"UGX": 3709.12772,
"TND": 3.136769,
"TOP": 2.36092,
"TRY": 32.836478,
"TTD": 6.7978,
"TWD": 32.560001,
"TZS": 2626.295328,
"UAH": 40.51595,
"UGX": 3711.539326,
"USD": 1,
"UYU": 39.322323,
"UZS": 12613.6,
"VES": 36.320372,
"VND": 25461.48991,
"UYU": 39.578963,
"UZS": 12585.732694,
"VES": 36.35908,
"VND": 25455.008685,
"VUV": 118.722,
"WST": 2.8,
"XAF": 612.41733,
"XAG": 0.03457994,
"XAU": 0.00043156,
"XAF": 612.876514,
"XAG": 0.0345453,
"XAU": 0.00042988,
"XCD": 2.70255,
"XDR": 0.761402,
"XOF": 612.41733,
"XPD": 0.00107841,
"XPF": 111.411003,
"XPT": 0.00101239,
"XDR": 0.759718,
"XOF": 612.876514,
"XPD": 0.00108638,
"XPF": 111.494537,
"XPT": 0.00101314,
"YER": 250.399984,
"ZAR": 18.227543,
"ZMW": 25.80335,
"ZAR": 18.4643,
"ZMW": 25.735569,
"ZWL": 322
}
}

View File

@ -6,10 +6,10 @@ import {
type RichTextContent,
type RichTextNode,
} from "src/shared/payload/payload-sdk";
import { cache } from "src/utils/payload";
import { contextCache } from "src/cache/contextCache";
export const formatLocale = (code: string): string =>
cache.locales.find(({ id }) => id === code)?.name ?? code;
contextCache.locales.find(({ id }) => id === code)?.name ?? code;
export const formatInlineTitle = ({
pretitle,

6
src/utils/logger.ts Normal file
View File

@ -0,0 +1,6 @@
export const getLogger = (prefix: string): Pick<Console, "log" | "error" | "warn" | "debug"> => ({
debug: (...message) => console.debug(prefix, ...message),
log: (...message) => console.log(prefix, ...message),
warn: (...message) => console.warn(prefix, ...message),
error: (...message) => console.error(prefix, ...message),
});

View File

@ -1,18 +1,9 @@
import {
type Language,
type EndpointWording,
type EndpointWebsiteConfig,
} from "src/shared/payload/payload-sdk";
import { dataCache } from "src/cache/dataCache";
import { getPayloadSDK } from "src/shared/payload/payload-sdk";
let token: string | undefined = undefined;
let expiration: number | undefined = undefined;
const responseCache = new Map<string, any>();
const idsCacheMap = new Map<string, Set<string>>();
const isPrecachingEnabled = import.meta.env.ENABLE_PRECACHING === "true";
export const payload = getPayloadSDK({
apiURL: import.meta.env.PAYLOAD_API_URL,
email: import.meta.env.PAYLOAD_USER,
@ -33,108 +24,5 @@ export const payload = getPayloadSDK({
console.log("[PayloadSDK] New token set. TTL is", diffInMinutes, "minutes.");
},
},
responseCache: {
get: (url) => {
const cachedResponse = responseCache.get(url);
if (cachedResponse) {
console.log("[ResponseCaching] Retrieved cache response for", url);
return structuredClone(cachedResponse);
}
},
set: (url, response) => {
const stringData = JSON.stringify(response);
const regex = /[a-f0-9]{24}/g;
const ids = [...stringData.matchAll(regex)].map((match) => match[0]);
const uniqueIds = [...new Set(ids)];
uniqueIds.forEach((id) => {
const current = idsCacheMap.get(id);
if (current) {
current.add(url);
} else {
idsCacheMap.set(id, new Set([url]));
}
});
console.log("[ResponseCaching] Caching response for", url);
responseCache.set(url, response);
},
},
responseCache: dataCache,
});
export const invalidateDataCache = async (ids: string[], urls: string[]) => {
const urlsToInvalidate = new Set<string>(urls);
ids.forEach((id) => {
const urlsForThisId = idsCacheMap.get(id);
if (!urlsForThisId) return;
idsCacheMap.delete(id);
[...urlsForThisId].forEach((url) => urlsToInvalidate.add(url));
});
for (const url of urlsToInvalidate) {
responseCache.delete(url);
console.log("[ResponseCaching][Invalidation] Deleted cache for", url);
try {
await payload.request(url);
} catch (e) {
console.log("[ResponseCaching][Revalidation] Failure for", url);
}
}
console.log("[ResponseCaching] There are currently", responseCache.size, "responses in cache.");
};
type Cache = {
locales: Language[];
currencies: string[];
wordings: EndpointWording[];
config: EndpointWebsiteConfig;
};
const fetchNewData = async (): Promise<Cache> => ({
locales: await payload.getLanguages(),
currencies: (await payload.getCurrencies()).map(({ id }) => id),
wordings: await payload.getWordings(),
config: await payload.getConfig(),
});
export let cache = await fetchNewData();
export const refreshWordings = async () => {
cache.wordings = await payload.getWordings();
};
export const refreshCurrencies = async () => {
cache.currencies = (await payload.getCurrencies()).map(({ id }) => id);
};
export const refreshLocales = async () => {
cache.locales = await payload.getLanguages();
};
export const refreshWebsiteConfig = async () => {
cache.config = await payload.getConfig();
};
let payloadInitialized = false;
export const initPayload = async () => {
if (!payloadInitialized) {
if (!isPrecachingEnabled) {
payloadInitialized = true;
return;
}
const { urls } = await payload.getAllSdkUrls();
for (const url of urls) {
try {
await payload.request(url);
} catch {
console.warn("[ResponseCaching] Precaching failed for url", url);
}
}
console.log("[ResponseCaching] Precaching completed!", responseCache.size, "responses cached");
payloadInitialized = true;
}
};