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

@@ -5,7 +5,7 @@ import {
useState,
type PointerEvent as ReactPointerEvent,
} from "react";
import { assetUrl, getJSON, itemsOrEmpty } from "../api";
import { assetUrl, getJSON, itemsOrEmpty, readJSONCache } from "../api";
import { langQuery, useI18n, type Lang } from "../i18n";
const FIGMA_ASSET_BASE = "/assets/ark-library/figma";
@@ -75,14 +75,19 @@ export function FigmaBanner() {
useEffect(() => {
let cancelled = false;
const bannersUrl = `/api/banners?lang=${bannerLangParam(lang)}`;
setActiveIndex(0);
getJSON<BannerApiResponse>(`/api/banners?lang=${bannerLangParam(lang)}`)
const cachedBanners = readJSONCache<BannerApiResponse>(bannersUrl);
if (cachedBanners) setSlides(toSlides(itemsOrEmpty(cachedBanners.items)));
getJSON<BannerApiResponse>(bannersUrl)
.then((res) => {
if (cancelled) return;
setSlides(toSlides(itemsOrEmpty(res.items)));
})
.catch(() => {
if (!cancelled) setSlides([]);
if (!cancelled && !cachedBanners) setSlides([]);
});
return () => {
cancelled = true;

View File

@@ -1,6 +1,6 @@
import { Info, Search as SearchIcon, X } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { getJSON, itemsOrEmpty } from "../api";
import { getJSON, itemsOrEmpty, readJSONCache } from "../api";
import { langQuery, type Lang } from "../i18n";
import type { Post, PostListResponse } from "../types/post";
import { MessageBubble } from "./messageStream/MessageBubble";
@@ -75,20 +75,22 @@ export function SearchPanel({
useEffect(() => {
let cancelled = false;
const tagsUrl = buildPostsUrl({
lang: langParam,
sort: "latest",
limit: TAG_SOURCE_LIMIT,
});
const cachedTags = readJSONCache<PostListResponse>(tagsUrl);
if (cachedTags) setTags(extractTags(itemsOrEmpty(cachedTags.items)));
setIsTagLoading(true);
getJSON<PostListResponse>(
buildPostsUrl({
lang: langParam,
sort: "latest",
limit: TAG_SOURCE_LIMIT,
}),
)
getJSON<PostListResponse>(tagsUrl)
.then((res) => {
if (cancelled) return;
setTags(extractTags(itemsOrEmpty(res.items)));
})
.catch(() => {
if (!cancelled) setTags([]);
if (!cancelled && !cachedTags) setTags([]);
})
.finally(() => {
if (!cancelled) setIsTagLoading(false);
@@ -102,17 +104,31 @@ export function SearchPanel({
const showTagPosts = (tag: string) => {
setSelectedTag(tag);
onQueryChange(tag);
const searchUrl = buildSearchUrl({
lang: langParam,
q: tag,
limit: TAG_RESULT_LIMIT,
});
const cachedPosts = readJSONCache<PostListResponse>(searchUrl);
if (cachedPosts) {
setTagPosts(
itemsOrEmpty(cachedPosts.items).filter((post) =>
post.tags?.some((postTag) => postTag.trim() === tag),
),
);
}
setIsPostLoading(true);
getJSON<PostListResponse>(
buildSearchUrl({ lang: langParam, q: tag, limit: TAG_RESULT_LIMIT }),
)
getJSON<PostListResponse>(searchUrl)
.then((res) => {
const exactMatches = itemsOrEmpty(res.items).filter((post) =>
post.tags?.some((postTag) => postTag.trim() === tag),
);
setTagPosts(exactMatches);
})
.catch(() => setTagPosts([]))
.catch(() => {
if (!cachedPosts) setTagPosts([]);
})
.finally(() => setIsPostLoading(false));
};

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { getJSON } from "../../../api";
import { getJSON, itemsOrEmpty, readJSONCache } from "../../../api";
import { langQuery, type Lang } from "../../../i18n";
import { sourceLanguageQuery } from "../../../i18nLanguages";
import { MOCK_POSTS } from "../../../mocks/mockPosts";
@@ -114,6 +114,8 @@ export function usePostStream(params: PostStreamParams): PostStreamResult {
setError(null);
const myReq = ++reqIdRef.current;
let showedCached = false;
try {
if (USE_MOCK) {
await new Promise((r) => setTimeout(r, MOCK_DELAY_MS));
@@ -133,9 +135,26 @@ export function usePostStream(params: PostStreamParams): PostStreamResult {
params,
resetting ? undefined : cursorRef.current,
);
if (resetting) {
const cachedPage = readJSONCache<PostListResponse>(url);
if (cachedPage && myReq === reqIdRef.current) {
showedCached = true;
const cachedItems = itemsOrEmpty(cachedPage.items);
setItems(cachedItems);
cursorRef.current = cachedPage.nextCursor;
const cachedMore = !!cachedPage.nextCursor;
setHasMore(cachedMore);
hasMoreRef.current = cachedMore;
}
}
const res = await getJSON<PostListResponse>(url);
if (myReq !== reqIdRef.current) return;
setItems((prev) => (resetting ? res.items : [...prev, ...res.items]));
const freshItems = itemsOrEmpty(res.items);
setItems((prev) =>
resetting ? freshItems : [...prev, ...freshItems],
);
cursorRef.current = res.nextCursor;
const more = !!res.nextCursor;
setHasMore(more);
@@ -143,7 +162,7 @@ export function usePostStream(params: PostStreamParams): PostStreamResult {
}
} catch (e) {
if (myReq !== reqIdRef.current) return;
setError(String(e));
if (!showedCached) setError(String(e));
} finally {
if (myReq === reqIdRef.current) setIsLoading(false);
loadingRef.current = false;