Add stale cache for public data

This commit is contained in:
TerryM
2026-05-28 23:09:18 +08:00
parent 5ae9647465
commit 320739f91b
8 changed files with 263 additions and 83 deletions

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { getJSON, itemsOrEmpty, type Category } from "../../api";
import { getJSON, itemsOrEmpty, readJSONCache, type Category } from "../../api";
import { CategoryIcon } from "../../components/CategoryIcon";
import { SectionHeader } from "../../components/SectionHeader";
import { langQuery, useI18n } from "../../i18n";
@@ -34,17 +34,32 @@ export function CategoriesPage() {
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
getJSON<Category[]>(
`/api/categories?lang=${encodeURIComponent(langQuery(lang))}`,
)
.then((items) =>
setCats(
itemsOrEmpty(items).sort(
(a, b) => figmaCategoryRank(a) - figmaCategoryRank(b),
),
let cancelled = false;
const categoriesUrl = `/api/categories?lang=${encodeURIComponent(
langQuery(lang),
)}`;
const applyCategories = (items: Category[]) =>
setCats(
itemsOrEmpty(items).sort(
(a, b) => figmaCategoryRank(a) - figmaCategoryRank(b),
),
)
.catch((e) => setErr(String(e)));
);
setErr(null);
const cachedCategories = readJSONCache<Category[]>(categoriesUrl);
if (cachedCategories) applyCategories(cachedCategories);
getJSON<Category[]>(categoriesUrl)
.then((items) => {
if (!cancelled) applyCategories(items);
})
.catch((e) => {
if (!cancelled && !cachedCategories) setErr(String(e));
});
return () => {
cancelled = true;
};
}, [lang]);
if (err) {

View File

@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { useParams } from "react-router-dom";
import { getJSON, itemsOrEmpty, type Category } from "../../api";
import { getJSON, itemsOrEmpty, readJSONCache, type Category } from "../../api";
import { AssetStreamPage } from "../../components/AssetStreamPage";
import { langQuery, useI18n } from "../../i18n";
import { cleanCategoryDisplayName } from "../../utils/categoryDisplay";
@@ -13,18 +13,32 @@ export function CategoryPage() {
useEffect(() => {
if (!slug) return;
setTitle(slug);
getJSON<Category[]>(
`/api/categories?lang=${encodeURIComponent(langQuery(lang))}`,
)
.then((cats) =>
setTitle(
cleanCategoryDisplayName(
itemsOrEmpty(cats).find((x) => x.slug === slug)?.name ?? slug,
),
let cancelled = false;
const categoriesUrl = `/api/categories?lang=${encodeURIComponent(
langQuery(lang),
)}`;
const applyTitle = (cats: Category[]) =>
setTitle(
cleanCategoryDisplayName(
itemsOrEmpty(cats).find((x) => x.slug === slug)?.name ?? slug,
),
)
.catch(() => setTitle(slug));
);
setTitle(slug);
const cachedCategories = readJSONCache<Category[]>(categoriesUrl);
if (cachedCategories) applyTitle(cachedCategories);
getJSON<Category[]>(categoriesUrl)
.then((cats) => {
if (!cancelled) applyTitle(cats);
})
.catch(() => {
if (!cancelled && !cachedCategories) setTitle(slug);
});
return () => {
cancelled = true;
};
}, [slug, lang]);
return <AssetStreamPage title={title || slug} scope={scope} />;

View File

@@ -1,7 +1,7 @@
import { ChevronLeft, ChevronRight } from "lucide-react";
import { Link, useLocation } from "react-router-dom";
import { useEffect, useRef, useState } from "react";
import { getJSON, itemsOrEmpty, type Category } from "../../api";
import { getJSON, itemsOrEmpty, readJSONCache, type Category } from "../../api";
import { CategoryIcon } from "../../components/CategoryIcon";
import { FigmaBanner } from "../../components/FigmaBanner";
import {
@@ -61,41 +61,80 @@ export function Home() {
const [recScroll, setRecScroll] = useState({ ratio: 1, progress: 0 });
useEffect(() => {
let cancelled = false;
const langParam = encodeURIComponent(langQuery(lang));
const languageParam = encodeURIComponent(sourceLanguageQuery(lang));
const catQ = `?lang=${langParam}`;
const postQ = `?lang=${langParam}&language=${languageParam}`;
const categoriesUrl = `/api/categories${catQ}`;
const recommendedUrl = `/api/posts/recommended${postQ}&limit=12`;
const latestUrl = `/api/posts${postQ}&sort=latest&limit=12`;
const popularUrl = `/api/posts${postQ}&sort=popular&limit=5`;
const applyHomeData = (
c: Category[],
r: { items: Post[] },
l: { items: Post[] },
p: { items: Post[] },
) => {
const categoryItems = itemsOrEmpty(c);
setCats(categoryItems);
setRec(
itemsOrEmpty(r.items).map((post) =>
postToResource(post, lang, categoryItems),
),
);
const latestItems = itemsOrEmpty(l.items);
setLatestPosts(latestItems);
setLatest(
latestItems.map((post) => postToResource(post, lang, categoryItems)),
);
const popularItems = itemsOrEmpty<Post>(p.items);
setPopularPosts(popularItems);
setPopular(
popularItems.map((post) => postToResource(post, lang, categoryItems)),
);
};
setErr(null);
const cachedCategories = readJSONCache<Category[]>(categoriesUrl);
const cachedRecommended = readJSONCache<{ items: Post[] }>(recommendedUrl);
const cachedLatest = readJSONCache<{ items: Post[] }>(latestUrl);
const cachedPopular = readJSONCache<{ items: Post[] }>(popularUrl);
const showedCached = !!(
cachedCategories &&
cachedRecommended &&
cachedLatest
);
if (showedCached) {
applyHomeData(
cachedCategories,
cachedRecommended,
cachedLatest,
cachedPopular ?? { items: [] },
);
}
Promise.all([
getJSON<Category[]>(`/api/categories${catQ}`),
getJSON<{ items: Post[] }>(`/api/posts/recommended${postQ}&limit=12`),
getJSON<{ items: Post[] }>(`/api/posts${postQ}&sort=latest&limit=12`),
getJSON<{ items: Post[] }>(
`/api/posts${postQ}&sort=popular&limit=5`,
).catch((): { items: Post[] } => ({ items: [] })),
getJSON<Category[]>(categoriesUrl),
getJSON<{ items: Post[] }>(recommendedUrl),
getJSON<{ items: Post[] }>(latestUrl),
getJSON<{ items: Post[] }>(popularUrl).catch(
(): { items: Post[] } => cachedPopular ?? { items: [] },
),
])
.then(([c, r, l, p]) => {
setCats(itemsOrEmpty(c));
setRec(
itemsOrEmpty(r.items).map((post) =>
postToResource(post, lang, itemsOrEmpty(c)),
),
);
const latestItems = itemsOrEmpty(l.items);
setLatestPosts(latestItems);
setLatest(
latestItems.map((post) =>
postToResource(post, lang, itemsOrEmpty(c)),
),
);
const popularItems = itemsOrEmpty<Post>(p.items);
setPopularPosts(popularItems);
setPopular(
popularItems.map((post) =>
postToResource(post, lang, itemsOrEmpty(c)),
),
);
if (cancelled) return;
applyHomeData(c, r, l, p);
})
.catch((e) => setErr(String(e)));
.catch((e) => {
if (!cancelled && !showedCached) setErr(String(e));
});
return () => {
cancelled = true;
};
}, [lang]);
const iconKeyForResource = (r: PostBackedResource) =>

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { getJSON, itemsOrEmpty, type Category } from "../../api";
import { getJSON, itemsOrEmpty, readJSONCache, type Category } from "../../api";
import { RecommendedCard } from "../../components/RecommendedCard";
import { SectionHeader } from "../../components/SectionHeader";
import { langQuery, useI18n } from "../../i18n";
@@ -16,23 +16,46 @@ export function OfficialRecommendationsPage() {
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
const langParam = encodeURIComponent(langQuery(lang));
const languageParam = encodeURIComponent(sourceLanguageQuery(lang));
const categoriesUrl = `/api/categories?lang=${langParam}`;
const recommendedUrl = `/api/posts/recommended?lang=${langParam}&language=${languageParam}&limit=100`;
const applyItems = (
categories: Category[],
recommended: { items: Post[] },
) => {
const cats = itemsOrEmpty(categories);
setItems(
itemsOrEmpty(recommended.items).map((post) =>
postToResource(post, lang, cats),
),
);
};
setErr(null);
const cachedCategories = readJSONCache<Category[]>(categoriesUrl);
const cachedRecommended = readJSONCache<{ items: Post[] }>(recommendedUrl);
const showedCached = !!(cachedCategories && cachedRecommended);
if (showedCached) applyItems(cachedCategories, cachedRecommended);
Promise.all([
getJSON<Category[]>(`/api/categories?lang=${langParam}`),
getJSON<{ items: Post[] }>(
`/api/posts/recommended?lang=${langParam}&language=${languageParam}&limit=100`,
),
getJSON<Category[]>(categoriesUrl),
getJSON<{ items: Post[] }>(recommendedUrl),
])
.then(([categories, recommended]) => {
const cats = itemsOrEmpty(categories);
setItems(
itemsOrEmpty(recommended.items).map((post) =>
postToResource(post, lang, cats),
),
);
if (cancelled) return;
applyItems(categories, recommended);
})
.catch((e) => setErr(String(e)));
.catch((e) => {
if (!cancelled && !showedCached) setErr(String(e));
});
return () => {
cancelled = true;
};
}, [lang]);
if (err) {