Use backlinks and rework the caching system

This commit is contained in:
DrMint 2024-07-26 09:22:33 +02:00
parent f1b37a31a9
commit 7dd91f5847
33 changed files with 240 additions and 280 deletions

View File

@ -3,7 +3,8 @@ import type {
EndpointWebsiteConfig,
EndpointWording,
} from "src/shared/payload/endpoint-types";
import type { PayloadSDK } from "src/shared/payload/sdk";
import { SDKEndpointNames, type PayloadSDK } from "src/shared/payload/sdk";
import type { EndpointChange } from "src/shared/payload/webhooks";
import { getLogger } from "src/utils/logger";
export class ContextCache {
@ -35,24 +36,42 @@ export class ContextCache {
await this.refreshWordings();
}
async refreshWordings() {
async invalidate(changes: EndpointChange[]) {
for (const change of changes) {
switch (change.type) {
case SDKEndpointNames.getWordings:
return await this.refreshWordings();
case SDKEndpointNames.getLanguages:
return await this.refreshLocales();
case SDKEndpointNames.getCurrencies:
return await this.refreshCurrencies();
case SDKEndpointNames.getWebsiteConfig:
return await this.refreshWebsiteConfig();
}
}
}
private async refreshWordings() {
this.wordings = (await this.payload.getWordings()).data;
this.logger.log("Wordings refreshed");
}
async refreshCurrencies() {
private async refreshCurrencies() {
this.currencies = (await this.payload.getCurrencies()).data.map(({ id }) => id);
this.logger.log("Currencies refreshed");
}
async refreshLocales() {
private async refreshLocales() {
this.languages = (await this.payload.getLanguages()).data;
this.locales = this.languages.filter(({ selectable }) => selectable).map(({ id }) => id);
this.logger.log("Locales refreshed");
}
async refreshWebsiteConfig() {
this.config = (await this.payload.getConfig()).data;
private async refreshWebsiteConfig() {
this.config = (await this.payload.getWebsiteConfig()).data;
this.logger.log("WebsiteConfig refreshed");
}
}

View File

@ -2,6 +2,7 @@ import { getLogger } from "src/utils/logger";
import { writeFile, mkdir, readFile } from "fs/promises";
import { existsSync } from "fs";
import type { PayloadSDK } from "src/shared/payload/sdk";
import type { EndpointChange } from "src/shared/payload/webhooks";
const ON_DISK_ROOT = `.cache/dataCache`;
const ON_DISK_RESPONSE_CACHE_FILE = `${ON_DISK_ROOT}/responseCache.json`;
@ -11,14 +12,12 @@ export class DataCache {
private initialized = false;
private readonly responseCache = new Map<string, any>();
private readonly invalidationMap = new Map<string, Set<string>>();
private scheduleSaveTimeout: NodeJS.Timeout | undefined;
constructor(
private readonly payload: PayloadSDK,
private readonly uncachedPayload: PayloadSDK,
private readonly onInvalidate: (urls: string[]) => Promise<void>
private readonly uncachedPayload: PayloadSDK
) {}
async init() {
@ -32,8 +31,8 @@ export class DataCache {
}
private async precache() {
// Get all keys from CMS
const allSDKUrls = (await this.uncachedPayload.getAllSdkUrls()).data.urls;
// Get all documents from CMS
const allDocs = (await this.uncachedPayload.getAll()).data;
// Load cache from disk if available
if (existsSync(ON_DISK_RESPONSE_CACHE_FILE)) {
@ -42,20 +41,20 @@ export class DataCache {
const data = JSON.parse(buffer.toString()) as [string, any][];
for (const [key, value] of data) {
// Do not include cache where the key is no longer in the CMS
if (!allSDKUrls.includes(key)) continue;
if (!allDocs.find(({ url }) => url === key)) continue;
this.set(key, value);
}
}
const cacheSizeBeforePrecaching = this.responseCache.size;
for (const url of allSDKUrls) {
for (const doc of allDocs) {
// Do not precache response if already included in the loaded cache from disk
if (this.responseCache.has(url)) continue;
if (this.responseCache.has(doc.url)) continue;
try {
await this.payload.request(url);
await this.payload.request(doc.url);
} catch {
this.logger.warn("Precaching failed for url", url);
this.logger.warn("Precaching failed for url", doc.url);
}
}
@ -77,20 +76,6 @@ export class DataCache {
set(url: string, response: any) {
if (import.meta.env.DATA_CACHING !== "true") return;
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.invalidationMap.get(id);
if (current) {
current.add(url);
} else {
this.invalidationMap.set(id, new Set([url]));
}
});
this.responseCache.set(url, response);
this.logger.log("Cached response for", url);
if (this.initialized) {
@ -98,18 +83,11 @@ export class DataCache {
}
}
async invalidate(ids: string[], urls: string[]) {
async invalidate(changes: EndpointChange[]) {
if (import.meta.env.DATA_CACHING !== "true") return;
const urlsToInvalidate = new Set<string>(urls);
ids.forEach((id) => {
const urlsForThisId = this.invalidationMap.get(id);
if (!urlsForThisId) return;
this.invalidationMap.delete(id);
[...urlsForThisId].forEach((url) => urlsToInvalidate.add(url));
});
for (const url of urlsToInvalidate) {
const urls = changes.map(({ url }) => url);
for (const url of urls) {
this.responseCache.delete(url);
this.logger.log("Invalidated cache for", url);
try {
@ -118,8 +96,6 @@ export class DataCache {
this.logger.log("Revalidation fails for", url);
}
}
this.onInvalidate([...urlsToInvalidate]);
this.logger.log("There are currently", this.responseCache.size, "responses in cache.");
if (this.initialized) {
this.scheduleSave();

143
src/cache/pageCache.ts vendored
View File

@ -6,22 +6,25 @@ import {
serializeResponse,
type SerializableResponse,
} from "src/utils/responses";
import type { PayloadSDK } from "src/shared/payload/sdk";
import { SDKEndpointNames, type PayloadSDK } from "src/shared/payload/sdk";
import type { EndpointChange } from "src/shared/payload/webhooks";
import type { ContextCache } from "src/cache/contextCache";
const ON_DISK_ROOT = `.cache/pageCache`;
const ON_DISK_RESPONSE_CACHE_FILE = `${ON_DISK_ROOT}/responseCache.json`;
const ON_DISK_INVALIDATION_MAP_FILE = `${ON_DISK_ROOT}/invalidationMap.json`;
export class PageCache {
private readonly logger = getLogger("[PageCache]");
private initialized = false;
private responseCache = new Map<string, Response>();
private invalidationMap = new Map<string, Set<string>>();
private scheduleSaveTimeout: NodeJS.Timeout | undefined;
constructor(private readonly uncachedPayload: PayloadSDK) {}
constructor(
private readonly uncachedPayload: PayloadSDK,
private readonly contextCache: ContextCache
) {}
async init() {
if (this.initialized) return;
@ -35,31 +38,16 @@ export class PageCache {
private async precache() {
if (import.meta.env.DATA_CACHING !== "true") return;
const { data: languages } = await this.uncachedPayload.getLanguages();
const locales = languages.filter(({ selectable }) => selectable).map(({ id }) => id);
// Get all pages urls from CMS
const allIds = (await this.uncachedPayload.getAllIds()).data;
const allPagesUrls = [
"/",
...allIds.audios.ids.map((id) => `/audios/${id}`),
...allIds.collectibles.slugs.map((slug) => `/collectibles/${slug}`),
...allIds.files.ids.map((id) => `/files/${id}`),
...allIds.folders.slugs.map((slug) => `/folders/${slug}`),
...allIds.images.ids.map((id) => `/images/${id}`),
...allIds.pages.slugs.map((slug) => `/pages/${slug}`),
...allIds.recorders.ids.map((id) => `/recorders/${id}`),
"/settings",
"/timeline",
...allIds.videos.ids.map((id) => `/videos/${id}`),
].flatMap((url) => locales.map((id) => `/${id}${url}`));
const allDocs = (await this.uncachedPayload.getAll()).data;
const allPageUrls = allDocs.flatMap((doc) => this.getUrlFromEndpointChange(doc));
// TODO: Add static pages likes "/" and "/settings"
// Load cache from disk if available
if (existsSync(ON_DISK_RESPONSE_CACHE_FILE) && existsSync(ON_DISK_INVALIDATION_MAP_FILE)) {
if (existsSync(ON_DISK_RESPONSE_CACHE_FILE)) {
this.logger.log("Loading cache from disk...");
// Handle RESPONSE_CACHE_FILE
{
const buffer = await readFile(ON_DISK_RESPONSE_CACHE_FILE);
const data = JSON.parse(buffer.toString()) as [string, SerializableResponse][];
let deserializedData = data.map<[string, Response]>(([key, value]) => [
@ -68,26 +56,14 @@ export class PageCache {
]);
// Do not include cache where the key is no longer in the CMS
deserializedData = deserializedData.filter(([key]) => allPagesUrls.includes(key));
deserializedData = deserializedData.filter(([key]) => allPageUrls.includes(key));
this.responseCache = new Map(deserializedData);
}
// Handle INVALIDATION_MAP_FILE
{
const buffer = await readFile(ON_DISK_INVALIDATION_MAP_FILE);
const data = JSON.parse(buffer.toString()) as [string, string[]][];
const deserialize = data.map<[string, Set<string>]>(([key, value]) => [
key,
new Set(value),
]);
this.invalidationMap = new Map(deserialize);
}
}
const cacheSizeBeforePrecaching = this.responseCache.size;
for (const url of allPagesUrls) {
for (const url of allPageUrls) {
// Do not precache response if already included in the loaded cache from disk
if (this.responseCache.has(url)) continue;
try {
@ -114,17 +90,8 @@ export class PageCache {
return;
}
set(url: string, response: Response, sdkCalls: string[]) {
set(url: string, response: Response) {
if (import.meta.env.PAGE_CACHING !== "true") return;
sdkCalls.forEach((id) => {
const current = this.invalidationMap.get(id);
if (current) {
current.add(url);
} else {
this.invalidationMap.set(id, new Set([url]));
}
});
this.responseCache.set(url, response.clone());
this.logger.log("Cached response for", url);
if (this.initialized) {
@ -132,16 +99,70 @@ export class PageCache {
}
}
async invalidate(sdkUrls: string[]) {
if (import.meta.env.PAGE_CACHING !== "true") return;
const pagesToInvalidate = new Set<string>();
private getUrlFromEndpointChange(change: EndpointChange): string[] {
const getUnlocalizedUrl = (): string[] => {
switch (change.type) {
case SDKEndpointNames.getFolder:
return [`/folders/${change.slug}`];
sdkUrls.forEach((url) => {
const pagesForThisSDKUrl = this.invalidationMap.get(url);
if (!pagesForThisSDKUrl) return;
this.invalidationMap.delete(url);
[...pagesForThisSDKUrl].forEach((page) => pagesToInvalidate.add(page));
});
case SDKEndpointNames.getCollectible:
return [`/collectibles/${change.slug}`];
case SDKEndpointNames.getCollectibleGallery:
return [`/collectibles/${change.slug}/gallery`];
case SDKEndpointNames.getCollectibleGalleryImage:
return [`/collectibles/${change.slug}/gallery/${change.index}`];
case SDKEndpointNames.getCollectibleScans:
return [`/collectibles/${change.slug}/scans`];
case SDKEndpointNames.getCollectibleScanPage:
return [`/collectibles/${change.slug}/scans/${change.index}`];
case SDKEndpointNames.getPage:
return [`/pages/${change.slug}`];
case SDKEndpointNames.getAudioByID:
return [`/audios/${change.id}`];
case SDKEndpointNames.getImageByID:
return [`/images/${change.id}`];
case SDKEndpointNames.getVideoByID:
return [`/videos/${change.id}`];
case SDKEndpointNames.getFileByID:
return [`/files/${change.id}`];
case SDKEndpointNames.getRecorderByID:
return [`/recorders/${change.id}`];
case SDKEndpointNames.getChronologyEvents:
case SDKEndpointNames.getChronologyEventByID:
return [`/timeline`];
case SDKEndpointNames.getWebsiteConfig:
case SDKEndpointNames.getLanguages:
case SDKEndpointNames.getCurrencies:
case SDKEndpointNames.getWordings:
return [...this.responseCache.keys()];
default:
return [];
}
};
return getUnlocalizedUrl().flatMap((url) =>
this.contextCache.locales.map((id) => `/${id}${url}`)
);
}
async invalidate(changes: EndpointChange[]) {
if (import.meta.env.PAGE_CACHING !== "true") return;
const pagesToInvalidate = new Set<string>(
changes.flatMap((change) => this.getUrlFromEndpointChange(change))
);
for (const url of pagesToInvalidate) {
this.responseCache.delete(url);
@ -181,13 +202,5 @@ export class PageCache {
encoding: "utf-8",
});
this.logger.log("Saved", ON_DISK_RESPONSE_CACHE_FILE);
const serializedIdsCache = JSON.stringify(
[...this.invalidationMap].map(([key, value]) => [key, [...value]])
);
await writeFile(ON_DISK_INVALIDATION_MAP_FILE, serializedIdsCache, {
encoding: "utf-8",
});
this.logger.log("Saved", ON_DISK_INVALIDATION_MAP_FILE);
}
}

View File

@ -4,12 +4,11 @@ import Topbar from "./components/Topbar/Topbar.astro";
import Footer from "./components/Footer.astro";
import AppLayoutBackgroundImg from "./components/AppLayoutBackgroundImg.astro";
import type { ComponentProps } from "astro/types";
import type { EndpointSource } from "src/shared/payload/endpoint-types";
import { getSDKEndpoint } from "src/shared/payload/sdk";
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
interface Props {
openGraph?: ComponentProps<typeof Html>["openGraph"];
parentPages?: EndpointSource[];
backlinks?: EndpointRelation[];
backgroundImage?: ComponentProps<typeof AppLayoutBackgroundImg>["img"] | undefined;
hideFooterLinks?: boolean;
hideHomeButton?: boolean;
@ -17,13 +16,9 @@ interface Props {
class?: string | undefined;
}
Astro.locals.sdkCalls.add(getSDKEndpoint.getCurrenciesEndpoint());
Astro.locals.sdkCalls.add(getSDKEndpoint.getLanguagesEndpoint());
Astro.locals.sdkCalls.add(getSDKEndpoint.getWordingsEndpoint());
const {
openGraph,
parentPages,
backlinks,
backgroundImage,
hideFooterLinks = false,
hideHomeButton = false,
@ -38,7 +33,7 @@ const {
{backgroundImage && <AppLayoutBackgroundImg img={backgroundImage} />}
<header>
<Topbar
parentPages={parentPages}
backlinks={backlinks}
hideHomeButton={hideHomeButton}
hideSearchButton={hideSearchButton}
/>

View File

@ -6,15 +6,15 @@ import LanguageSelector from "./components/LanguageSelector.astro";
import CurrencySelector from "./components/CurrencySelector.astro";
import ParentPagesButton from "./components/ParentPagesButton.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointSource } from "src/shared/payload/endpoint-types";
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
interface Props {
parentPages?: EndpointSource[] | undefined;
backlinks?: EndpointRelation[] | undefined;
hideHomeButton?: boolean;
hideSearchButton?: boolean;
}
const { parentPages = [], hideHomeButton = false, hideSearchButton = false } = Astro.props;
const { backlinks = [], hideHomeButton = false, hideSearchButton = false } = Astro.props;
const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
---
@ -23,14 +23,14 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
<nav id="topbar" class="when-no-print">
{
(!hideHomeButton || parentPages.length > 0) && (
(!hideHomeButton || backlinks.length > 0) && (
<div id="left" class="hide-scrollbar">
<a href={getLocalizedUrl("/")} class="pressable-label">
<Icon name="material-symbols:home" width={16} height={16} />
<p>{t("home.title")}</p>
</a>
{parentPages.length > 0 && <ParentPagesButton parentPages={parentPages} />}
{backlinks.length > 0 && <ParentPagesButton backlinks={backlinks} />}
</div>
)
}

View File

@ -3,14 +3,14 @@ import Tooltip from "components/Tooltip.astro";
import { Icon } from "astro-icon/components";
import { getI18n } from "src/i18n/i18n";
import ReturnToButton from "./ReturnToButton.astro";
import SourceRow from "components/SourceRow.astro";
import type { EndpointSource } from "src/shared/payload/endpoint-types";
import RelationRow from "components/RelationRow.astro";
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
interface Props {
parentPages: EndpointSource[];
backlinks: EndpointRelation[];
}
const { parentPages } = Astro.props;
const { backlinks } = Astro.props;
const { t } = await getI18n(Astro.locals.currentLocale);
---
@ -18,15 +18,15 @@ const { t } = await getI18n(Astro.locals.currentLocale);
{/* ------------------------------------------- HTML ------------------------------------------- */}
{
parentPages.length === 1 && parentPages[0] ? (
<ReturnToButton parentPage={parentPages[0]} />
backlinks.length === 1 && backlinks[0] ? (
<ReturnToButton relation={backlinks[0]} />
) : (
<Tooltip trigger="click" class="when-js">
<div id="tooltip-content" slot="tooltip-content">
<p>{t("header.nav.parentPages.tooltip")}</p>
<div>
{parentPages.map((parentPage) => (
<SourceRow source={parentPage} />
{backlinks.map((relation) => (
<RelationRow relation={relation} />
))}
</div>
</div>
@ -34,7 +34,7 @@ const { t } = await getI18n(Astro.locals.currentLocale);
<Icon name="material-symbols:keyboard-return" />
<p>
{t("header.nav.parentPages.label", {
count: parentPages.length,
count: backlinks.length,
})}
</p>
</button>

View File

@ -1,14 +1,14 @@
---
import { Icon } from "astro-icon/components";
import { getI18n } from "src/i18n/i18n";
import type { EndpointSource } from "src/shared/payload/endpoint-types";
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
interface Props {
parentPage: EndpointSource;
relation: EndpointRelation;
}
const { parentPage } = Astro.props;
const { formatEndpointSource } = await getI18n(Astro.locals.currentLocale);
const { relation } = Astro.props;
const { formatEndpointRelation } = await getI18n(Astro.locals.currentLocale);
const {
href,
@ -17,7 +17,7 @@ const {
target = undefined,
rel = undefined,
lang,
} = formatEndpointSource(parentPage);
} = formatEndpointRelation(relation);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}

View File

@ -1,6 +1,7 @@
---
import GenericPreview from "components/Previews/GenericPreview.astro";
import { getI18n } from "src/i18n/i18n";
import { Collections } from "src/shared/payload/constants";
import type { EndpointFolder } from "src/shared/payload/endpoint-types";
import type { Attribute } from "src/utils/attributes";
@ -11,7 +12,7 @@ interface Props {
const { getLocalizedUrl, getLocalizedMatch, t } = await getI18n(Astro.locals.currentLocale);
const {
folder: { translations, slug, files, sections, parentPages },
folder: { translations, slug, files, sections, backlinks },
} = Astro.props;
const { language, title } = getLocalizedMatch(translations);
@ -32,9 +33,9 @@ const attributes: Attribute[] = [
{
icon: "material-symbols:keyboard-return",
title: t("global.folders.attributes.parent"),
values: parentPages.flatMap((parent) => {
if (parent.type !== "folder") return [];
const name = getLocalizedMatch(parent.folder.translations).title;
values: backlinks.flatMap((link) => {
if (link.type !== Collections.Folders) return [];
const name = getLocalizedMatch(link.value.translations).title;
return { name };
}),
},

View File

@ -1,13 +1,13 @@
---
import { getI18n } from "src/i18n/i18n";
import type { EndpointSource } from "src/shared/payload/endpoint-types";
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
interface Props {
source: EndpointSource;
relation: EndpointRelation;
}
const { source } = Astro.props;
const { formatEndpointSource } = await getI18n(Astro.locals.currentLocale);
const { relation } = Astro.props;
const { formatEndpointRelation } = await getI18n(Astro.locals.currentLocale);
const {
href,
@ -16,7 +16,7 @@ const {
target = undefined,
rel = undefined,
lang,
} = formatEndpointSource(source);
} = formatEndpointRelation(relation);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}

1
src/env.d.ts vendored
View File

@ -4,7 +4,6 @@
declare namespace App {
interface Locals {
currentLocale: string;
sdkCalls: Set<string>;
pageCaching: boolean;
}
}

View File

@ -1,7 +1,8 @@
import type { WordingKey } from "src/i18n/wordings-keys";
import { contextCache } from "src/services";
import { capitalize, formatInlineTitle } from "src/utils/format";
import type { EndpointChronologyEvent, EndpointSource } from "src/shared/payload/endpoint-types";
import type { EndpointChronologyEvent, EndpointRelation } from "src/shared/payload/endpoint-types";
import { Collections } from "src/shared/payload/constants";
export const defaultLocale = "en";
@ -236,8 +237,8 @@ export const getI18n = async (locale: string) => {
}
};
const formatEndpointSource = (
source: EndpointSource
const formatEndpointRelation = (
relation: EndpointRelation
): {
href: string;
typeLabel: string;
@ -246,98 +247,84 @@ export const getI18n = async (locale: string) => {
target?: string;
rel?: string;
} => {
switch (source.type) {
case "url": {
switch (relation.type) {
case "url":
return {
href: source.url,
href: relation.url,
typeLabel: t("global.sources.typeLabel.url"),
label: source.label,
label: relation.label,
target: "_blank",
rel: "noopener noreferrer",
};
}
case "collectible": {
const rangeLabel = (() => {
switch (source.range?.type) {
case Collections.Collectibles: {
const getRangeLabel = () => {
switch (relation.range?.type) {
case "timestamp":
return t("global.sources.typeLabel.collectible.range.timestamp", {
page: source.range.timestamp,
page: relation.range.timestamp,
});
case "page":
return t("global.sources.typeLabel.collectible.range.page", {
page: source.range.page,
page: relation.range.page,
});
case "custom":
return t("global.sources.typeLabel.collectible.range.custom", {
note: getLocalizedMatch(source.range.translations).note,
note: getLocalizedMatch(relation.range.translations).note,
});
case undefined:
default:
return "";
}
})();
};
const translation = getLocalizedMatch(source.collectible.translations);
const translation = getLocalizedMatch(relation.value.translations);
return {
href: getLocalizedUrl(`/collectibles/${source.collectible.slug}`),
href: getLocalizedUrl(`/collectibles/${relation.value.slug}`),
typeLabel: t("global.sources.typeLabel.collectible"),
label: formatInlineTitle(translation) + rangeLabel,
label: formatInlineTitle(translation) + getRangeLabel(),
lang: translation.language,
};
}
case "page": {
const translation = getLocalizedMatch(source.page.translations);
case Collections.Pages: {
const translation = getLocalizedMatch(relation.value.translations);
return {
href: getLocalizedUrl(`/pages/${source.page.slug}`),
href: getLocalizedUrl(`/pages/${relation.value.slug}`),
typeLabel: t("global.sources.typeLabel.page"),
label: formatInlineTitle(translation),
lang: translation.language,
};
}
case "folder": {
const translation = getLocalizedMatch(source.folder.translations);
case Collections.Folders: {
const translation = getLocalizedMatch(relation.value.translations);
return {
href: getLocalizedUrl(`/folders/${source.folder.slug}`),
href: getLocalizedUrl(`/folders/${relation.value.slug}`),
typeLabel: t("global.sources.typeLabel.folder"),
label: formatInlineTitle(translation),
lang: translation.language,
};
}
case "scans": {
const translation = getLocalizedMatch(source.collectible.translations);
return {
href: getLocalizedUrl(`/collectibles/${source.collectible.slug}/scans`),
typeLabel: t("global.sources.typeLabel.scans"),
label: formatInlineTitle(translation),
lang: translation.language,
};
}
case "gallery": {
const translation = getLocalizedMatch(source.collectible.translations);
return {
href: getLocalizedUrl(`/collectibles/${source.collectible.slug}/gallery`),
typeLabel: t("global.sources.typeLabel.gallery"),
label: formatInlineTitle(translation),
lang: translation.language,
};
}
default: {
/* TODO: Handle other types of relations */
case Collections.Audios:
case Collections.ChronologyEvents:
case Collections.Files:
case Collections.Images:
case Collections.Recorders:
case Collections.Tags:
case Collections.Videos:
default:
return {
href: "/404",
label: `Invalid type ${source["type"]}`,
label: `Invalid type ${relation["type"]}`,
typeLabel: "Error",
};
}
}
};
return {
@ -353,7 +340,7 @@ export const getI18n = async (locale: string) => {
formatMillimeters,
formatNumber,
formatTimelineDate,
formatEndpointSource,
formatEndpointRelation,
formatScanIndexShort,
formatFilesize,
};

View File

@ -26,7 +26,7 @@ export const pageCachingMiddleware = defineMiddleware(async ({ url, request, loc
response.headers.set("Last-Modified", new Date().toUTCString());
if (locals.pageCaching) {
pageCache.set(pathname, response, [...locals.sdkCalls]);
pageCache.set(pathname, response);
}
}

View File

@ -3,7 +3,6 @@ import { getCurrentLocale } from "src/middleware/utils";
export const setAstroLocalsMiddleware = defineMiddleware(async ({ url, locals }, next) => {
locals.currentLocale = getCurrentLocale(url.pathname) ?? "en";
locals.sdkCalls = new Set();
locals.pageCaching = true;
return next();
});

View File

@ -1,7 +1,6 @@
import type { APIRoute } from "astro";
import { Collections } from "src/shared/payload/constants";
import type { AfterOperationWebHookMessage } from "src/shared/payload/webhooks";
import { contextCache, dataCache } from "src/services.ts";
import { contextCache, dataCache, pageCache } from "src/services.ts";
import type { EndpointChange } from "src/shared/payload/webhooks";
export const POST: APIRoute = async ({ request, locals }) => {
locals.pageCaching = false;
@ -11,38 +10,17 @@ export const POST: APIRoute = async ({ request, locals }) => {
return new Response(null, { status: 403, statusText: "Forbidden" });
}
const message = (await request.json()) as AfterOperationWebHookMessage;
console.log("[Webhook] Received messages from CMS:", message);
const changes = (await request.json()) as EndpointChange[];
console.log("[Webhook] Received messages from CMS:", changes);
// Not awaiting on purpose to respond with a 202 and not block the CMS
handleWebHookMessage(message);
handleWebHookMessage(changes);
return new Response(null, { status: 202, statusText: "Accepted" });
};
const handleWebHookMessage = async ({
addedDependantIds,
collection,
urls,
id,
}: AfterOperationWebHookMessage) => {
await dataCache.invalidate([...(id ? [id] : []), ...addedDependantIds], urls);
switch (collection) {
case Collections.Wordings:
await contextCache.refreshWordings();
break;
case Collections.Currencies:
await contextCache.refreshCurrencies();
break;
case Collections.Languages:
await contextCache.refreshLocales();
break;
case Collections.WebsiteConfig:
await contextCache.refreshWebsiteConfig();
break;
}
const handleWebHookMessage = async (changes: EndpointChange[]) => {
await dataCache.invalidate(changes);
await contextCache.invalidate(changes);
await pageCache.invalidate(changes);
};

View File

@ -1,9 +1,10 @@
import type { APIRoute } from "astro";
import { dataCache, pageCache } from "src/services";
import { contextCache, dataCache, pageCache } from "src/services";
export const GET: APIRoute = async ({ locals }) => {
locals.pageCaching = false;
await contextCache.init();
await dataCache.init();
await pageCache.init();
return new Response(null, { status: 200, statusText: "Ok" });

View File

@ -32,7 +32,6 @@ const { translations, thumbnail, createdAt, updatedAt, updatedBy, attributes } =
await (async () => {
if (Astro.props.page) return Astro.props.page;
const response = await payload.getPage(slug);
Astro.locals.sdkCalls.add(response.endpointCalled);
return response.data;
})();

View File

@ -29,7 +29,6 @@ const index = Astro.props.index ?? parseInt(reqUrl.searchParams.get("index")!);
const event = await (async () => {
if (Astro.props.event) return Astro.props.event;
const response = await payload.getChronologyEventByID(id);
Astro.locals.sdkCalls.add(response.endpointCalled);
return response.data;
})();
const { sources, translations } = event.events[index]!;

View File

@ -17,7 +17,6 @@ const response = await fetchOr404(() => payload.getAudioByID(id));
if (response instanceof Response) {
return response;
}
Astro.locals.sdkCalls.add(response.endpointCalled);
const audio = response.data;
const { getLocalizedMatch, t, formatFilesize, formatDate } = await getI18n(
@ -34,6 +33,7 @@ const {
updatedAt,
thumbnail,
mimeType,
backlinks,
} = audio;
const { pretitle, title, subtitle, description, language } = getLocalizedMatch(translations);
@ -73,6 +73,7 @@ const metaAttributes = [
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppLayout
backlinks={backlinks}
openGraph={{
title: formatInlineTitle({ pretitle, title, subtitle }),
description: description && formatRichTextToString(description),

View File

@ -17,8 +17,7 @@ const response = await fetchOr404(() => payload.getCollectibleGalleryImage(slug,
if (response instanceof Response) {
return response;
}
Astro.locals.sdkCalls.add(response.endpointCalled);
const { parentPages, previousIndex, nextIndex, image } = response.data;
const { backlinks, previousIndex, nextIndex, image } = response.data;
const { filename, translations, createdAt, updatedAt, credits, attributes, mimeType } = image;
@ -67,7 +66,7 @@ const metaAttributes = [
description: description && formatRichTextToString(description),
thumbnail: image,
}}
parentPages={parentPages}>
backlinks={backlinks}>
<Lightbox
image={image}
pretitle={pretitle}

View File

@ -15,8 +15,7 @@ const response = await fetchOr404(() => payload.getCollectibleGallery(slug));
if (response instanceof Response) {
return response;
}
Astro.locals.sdkCalls.add(response.endpointCalled);
const { translations, parentPages, images, thumbnail } = response.data;
const { translations, backlinks, images, thumbnail } = response.data;
const translation = getLocalizedMatch(translations);
---
@ -29,7 +28,7 @@ const translation = getLocalizedMatch(translations);
description: translation.description && formatRichTextToString(translation.description),
thumbnail,
}}
parentPages={parentPages}
backlinks={backlinks}
class="app">
<AppLayoutTitle
title={translation.title}

View File

@ -30,7 +30,6 @@ const response = await fetchOr404(() => payload.getCollectible(slug));
if (response instanceof Response) {
return response;
}
Astro.locals.sdkCalls.add(response.endpointCalled);
const collectible = response.data;
const {
@ -47,7 +46,7 @@ const {
scans,
subitems,
files,
parentPages,
backlinks,
attributes,
contents,
createdAt,
@ -128,7 +127,7 @@ if (languages.length > 0) {
description: description && formatRichTextToString(description),
thumbnail,
}}
parentPages={parentPages}
backlinks={backlinks}
backgroundImage={backgroundImage ?? thumbnail}>
<AsideLayout reducedAsideWidth>
<Fragment slot="header">

View File

@ -16,8 +16,7 @@ const response = await fetchOr404(() => payload.getCollectibleScanPage(slug, ind
if (response instanceof Response) {
return response;
}
Astro.locals.sdkCalls.add(response.endpointCalled);
const { parentPages, previousIndex, nextIndex, image, translations } = response.data;
const { backlinks, previousIndex, nextIndex, image, translations } = response.data;
const translation = getLocalizedMatch(translations);
---
@ -29,7 +28,7 @@ const translation = getLocalizedMatch(translations);
title: `${formatInlineTitle(translation)} (${index})`,
description: translation.description && formatRichTextToString(translation.description),
}}
parentPages={parentPages}>
backlinks={backlinks}>
<Lightbox
image={image}
title={formatScanIndexShort(index)}

View File

@ -16,8 +16,7 @@ const response = await fetchOr404(() => payload.getCollectibleScans(slug));
if (response instanceof Response) {
return response;
}
Astro.locals.sdkCalls.add(response.endpointCalled);
const { translations, credits, cover, pages, dustjacket, obi, parentPages, thumbnail } =
const { translations, credits, cover, pages, dustjacket, obi, backlinks, thumbnail } =
response.data;
const translation = getLocalizedMatch(translations);
@ -46,7 +45,7 @@ const hasOutsideObi = obi ? Object.keys(obi).some((value) => !value.includes("in
description: translation.description && formatRichTextToString(translation.description),
thumbnail,
}}
parentPages={parentPages}
backlinks={backlinks}
class="app">
<AppLayoutTitle
title={translation.title}

View File

@ -18,7 +18,6 @@ const response = await fetchOr404(() => payload.getFileByID(id));
if (response instanceof Response) {
return response;
}
Astro.locals.sdkCalls.add(response.endpointCalled);
const {
translations,
attributes,
@ -30,6 +29,7 @@ const {
updatedAt,
createdAt,
thumbnail,
backlinks,
} = response.data;
const { getLocalizedMatch, t, formatFilesize, formatDate } = await getI18n(
@ -84,6 +84,7 @@ const smallTitle = title === filename;
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppLayout
backlinks={backlinks}
openGraph={{
title: formatInlineTitle({ pretitle, title, subtitle }),
description: description && formatRichTextToString(description),

View File

@ -22,8 +22,7 @@ const response = await fetchOr404(() => payload.getFolder(slug));
if (response instanceof Response) {
return response;
}
Astro.locals.sdkCalls.add(response.endpointCalled);
const { files, parentPages, sections, translations } = response.data;
const { files, backlinks, sections, translations } = response.data;
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { language, title, description } = getLocalizedMatch(translations);
@ -36,7 +35,7 @@ const { language, title, description } = getLocalizedMatch(translations);
title: title,
description: description && formatRichTextToString(description),
}}
parentPages={parentPages}
backlinks={backlinks}
class="app">
<AppLayoutTitle title={title} lang={language} />
{description && <RichText content={description} context={{ lang: language }} />}

View File

@ -12,7 +12,6 @@ const response = await fetchOr404(() => payload.getImageByID(id));
if (response instanceof Response) {
return response;
}
Astro.locals.sdkCalls.add(response.endpointCalled);
const image = response.data;
const {
filename,
@ -25,6 +24,7 @@ const {
filesize,
width,
height,
backlinks,
} = image;
const { getLocalizedMatch, formatDate, t, formatFilesize, formatNumber } = await getI18n(
@ -83,6 +83,7 @@ const metaAttributes = [
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppLayout
backlinks={backlinks}
openGraph={{
title: formatInlineTitle({ pretitle, title, subtitle }),
description: description && formatRichTextToString(description),

View File

@ -12,9 +12,8 @@ const response = await fetchOr404(() => payload.getPage(slug));
if (response instanceof Response) {
return response;
}
Astro.locals.sdkCalls.add(response.endpointCalled);
const page = response.data;
const { parentPages, thumbnail, translations, backgroundImage } = page;
const { backlinks, thumbnail, translations, backgroundImage } = page;
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const meta = getLocalizedMatch(translations);
@ -28,7 +27,7 @@ const meta = getLocalizedMatch(translations);
description: meta.summary && formatRichTextToString(meta.summary),
thumbnail: thumbnail,
}}
parentPages={parentPages}
backlinks={backlinks}
backgroundImage={backgroundImage ?? thumbnail}>
<Page slug={slug} lang={Astro.locals.currentLocale} page={page} />
</AppLayout>

View File

@ -16,7 +16,6 @@ const response = await fetchOr404(() => payload.getRecorderByID(id));
if (response instanceof Response) {
return response;
}
Astro.locals.sdkCalls.add(response.endpointCalled);
const { username, languages, avatar, translations } = response.data;
const { t, getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);

View File

@ -1,12 +1,12 @@
---
import { Icon } from "astro-icon/components";
import SourceRow from "components/SourceRow.astro";
import RelationRow from "components/RelationRow.astro";
import Tooltip from "components/Tooltip.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointSource } from "src/shared/payload/endpoint-types";
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
interface Props {
sources: EndpointSource[];
sources: EndpointRelation[];
}
const { sources } = Astro.props;
@ -26,7 +26,7 @@ const { t } = await getI18n(Astro.locals.currentLocale);
<Tooltip trigger="click">
<div id="tooltip-content" slot="tooltip-content">
{sources.map((source) => (
<SourceRow source={source} />
<RelationRow relation={source} />
))}
</div>
<button class="pressable-label">

View File

@ -14,7 +14,6 @@ const response = await fetchOr404(() => payload.getChronologyEvents());
if (response instanceof Response) {
return response;
}
Astro.locals.sdkCalls.add(response.endpointCalled);
const events = response.data;
const groupedEvents = groupBy(events, (event) => event.date.year);

View File

@ -17,7 +17,6 @@ const response = await fetchOr404(() => payload.getVideoByID(id));
if (response instanceof Response) {
return response;
}
Astro.locals.sdkCalls.add(response.endpointCalled);
const video = response.data;
const {
translations,
@ -30,6 +29,7 @@ const {
createdAt,
thumbnail,
mimeType,
backlinks,
} = video;
const { getLocalizedMatch, t, formatFilesize, formatDate } = await getI18n(
@ -73,6 +73,7 @@ const metaAttributes = [
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppLayout
backlinks={backlinks}
openGraph={{
title: formatInlineTitle({ pretitle, title, subtitle }),
description: description && formatRichTextToString(description),

View File

@ -22,6 +22,13 @@ export const analytics = import.meta.env.ANALYTICS_URL
const tokenCache = new TokenCache();
const uncachedPayload = new PayloadSDK(
import.meta.env.PAYLOAD_API_URL,
import.meta.env.PAYLOAD_USER,
import.meta.env.PAYLOAD_PASSWORD
);
uncachedPayload.addTokenCache(tokenCache);
export const payload = new PayloadSDK(
import.meta.env.PAYLOAD_API_URL,
import.meta.env.PAYLOAD_USER,
@ -29,19 +36,11 @@ export const payload = new PayloadSDK(
);
payload.addTokenCache(tokenCache);
const uncachedPayload = new PayloadSDK(
import.meta.env.PAYLOAD_API_URL,
import.meta.env.PAYLOAD_USER,
import.meta.env.PAYLOAD_PASSWORD
);
uncachedPayload.addTokenCache(tokenCache);
export const pageCache = new PageCache(uncachedPayload);
// Loading context cache first so that the server can still serve responses while precaching.
export const contextCache = new ContextCache(payload);
await contextCache.init();
export const dataCache = new DataCache(payload, uncachedPayload, (urls) =>
pageCache.invalidate(urls)
);
export const dataCache = new DataCache(payload, uncachedPayload);
payload.addDataCache(dataCache);
export const pageCache = new PageCache(uncachedPayload, contextCache);

@ -1 +1 @@
Subproject commit 47c990080173a2330f0c7a9837dac34dba4e0811
Subproject commit caa79dee9eca5b9b6959e6f5a721245202423612