fix(rates): reduce cache TTL, expose fallback header and show stale data warning
This commit is contained in:
@@ -5,7 +5,7 @@ import {
|
|||||||
RATES_CACHE_CONTROL_VALUE,
|
RATES_CACHE_CONTROL_VALUE,
|
||||||
} from "@/lib/server/rates-cache";
|
} from "@/lib/server/rates-cache";
|
||||||
|
|
||||||
export const revalidate = 300;
|
export const revalidate = 60;
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@@ -19,13 +19,18 @@ export async function GET() {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const cachedRates = getLastCachedRates();
|
const cachedRates = getLastCachedRates();
|
||||||
|
const fallbackErrorMessage =
|
||||||
|
error instanceof Error
|
||||||
|
? error.message.replace(/[\r\n]+/g, " ")
|
||||||
|
: "Upstream provider error";
|
||||||
|
|
||||||
if (cachedRates) {
|
if (cachedRates) {
|
||||||
return NextResponse.json(cachedRates, {
|
return NextResponse.json(cachedRates, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
"Cache-Control": RATES_CACHE_CONTROL_VALUE,
|
"Cache-Control": RATES_CACHE_CONTROL_VALUE,
|
||||||
"X-Cache-Fallback": "stale-on-error"
|
"X-Cache-Fallback": "stale-on-error",
|
||||||
|
"X-Cache-Fallback-Error": fallbackErrorMessage
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,6 +243,24 @@ export function ConverterCard({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const displayUpdatedAt = useMemo(() => {
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamps = [new Date(data.updatedAt).getTime()];
|
||||||
|
|
||||||
|
if (marketData?.updatedAt) {
|
||||||
|
timestamps.push(new Date(marketData.updatedAt).getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
const latest = Math.max(
|
||||||
|
...timestamps.filter((timestamp) => Number.isFinite(timestamp)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Number.isFinite(latest) ? new Date(latest).toISOString() : data.updatedAt;
|
||||||
|
}, [data, marketData?.updatedAt]);
|
||||||
|
|
||||||
if (isLoading && !data) {
|
if (isLoading && !data) {
|
||||||
return <ConverterSkeleton />;
|
return <ConverterSkeleton />;
|
||||||
}
|
}
|
||||||
@@ -497,7 +515,7 @@ export function ConverterCard({
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-xs uppercase tracking-[0.12em]">Last updated</p>
|
<p className="text-xs uppercase tracking-[0.12em]">Last updated</p>
|
||||||
<p className="mt-1 text-sm text-foreground">
|
<p className="mt-1 text-sm text-foreground">
|
||||||
{formatTimestamp(data.updatedAt)}
|
{formatTimestamp(displayUpdatedAt ?? data.updatedAt)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,9 +28,11 @@ export function useMarketRates() {
|
|||||||
|
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
const parsed = parseRatesResponse(payload);
|
const parsed = parseRatesResponse(payload);
|
||||||
|
const isFallback = response.headers.get("X-Cache-Fallback") === "stale-on-error";
|
||||||
|
const fallbackError = response.headers.get("X-Cache-Fallback-Error");
|
||||||
|
|
||||||
setData(parsed);
|
setData(parsed);
|
||||||
setError(null);
|
setError(isFallback ? (fallbackError ?? "Upstream provider error") : null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : "Unknown error";
|
const message = err instanceof Error ? err.message : "Unknown error";
|
||||||
setError(message);
|
setError(message);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { fetchUnifiedRates } from "@/lib/api/normalize";
|
import { fetchUnifiedRates } from "@/lib/api/normalize";
|
||||||
import type { RatesResponse } from "@/lib/rates";
|
import type { RatesResponse } from "@/lib/rates";
|
||||||
|
|
||||||
export const RATES_CACHE_TTL_MS = 300_000;
|
export const RATES_CACHE_TTL_MS = 60_000;
|
||||||
export const RATES_CACHE_CONTROL_VALUE =
|
export const RATES_CACHE_CONTROL_VALUE =
|
||||||
"s-maxage=300, stale-while-revalidate=1800";
|
"s-maxage=60, stale-while-revalidate=600";
|
||||||
|
|
||||||
let cachedRates: RatesResponse | null = null;
|
let cachedRates: RatesResponse | null = null;
|
||||||
let cacheTimestamp = 0;
|
let cacheTimestamp = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user