1
mirror of https://github.com/jakejarvis/domainstack.io.git synced 2025-12-02 19:33:48 -05:00

refactor: update caching strategy across multiple modules to use Next.js Data Cache with revalidation settings

This commit is contained in:
2025-11-23 20:32:41 -05:00
parent b6e520131a
commit 793a6700cb
6 changed files with 49 additions and 44 deletions

View File

@@ -77,7 +77,7 @@
- Vercel Blob backs favicon/screenshot storage with automatic public URLs; metadata cached in Postgres.
- Screenshots (Puppeteer): prefer `puppeteer-core` + `@sparticuz/chromium` on Vercel.
- Persist domain data in Postgres via Drizzle with per-table TTL columns (`expiresAt`).
- All caching uses Next.js Data Cache (`"use cache"` directive) or Postgres.
- All caching uses Next.js Data Cache (`fetch` with `next: { revalidate }`) or Postgres.
- Database connections: Use Vercel's Postgres connection pooling (`@vercel/postgres`) for optimal performance.
- Background revalidation: Event-driven via Inngest functions in `lib/inngest/functions/` with built-in concurrency control.
- Use Next.js 16 `after()` for fire-and-forget background operations (analytics, domain access tracking) with graceful degradation.

View File

@@ -1,24 +1,26 @@
import { cacheLife } from "next/cache";
import { Button } from "@/components/ui/button";
import { REPOSITORY_SLUG } from "@/lib/constants/app";
async function fetchRepoStars(): Promise<number | null> {
"use cache";
cacheLife("hours");
try {
const res = await fetch(
"https://api.github.com/repos/jakejarvis/domainstack.io",
{
headers: {
Accept: "application/vnd.github+json",
...(process.env.GITHUB_TOKEN
? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` }
: {}),
},
const res = await fetch(`https://api.github.com/repos/${REPOSITORY_SLUG}`, {
headers: {
Accept: "application/vnd.github+json",
// token is optional but allows for more frequent/reliable API calls
...(process.env.GITHUB_TOKEN
? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` }
: {}),
},
);
next: {
revalidate: 3600, // 1 hour
tags: ["github-stars"],
},
});
if (!res.ok) return null;
const json = (await res.json()) as { stargazers_count?: number };
return typeof json.stargazers_count === "number"
? json.stargazers_count
: null;
@@ -29,12 +31,18 @@ async function fetchRepoStars(): Promise<number | null> {
export async function GithubStars() {
const stars = await fetchRepoStars();
const label = stars === null ? "0" : `${stars}`;
const label =
stars === null
? "0"
: new Intl.NumberFormat("en-US", {
notation: "compact",
compactDisplay: "short",
}).format(stars);
return (
<Button variant="ghost" size="sm" asChild>
<a
href="https://github.com/jakejarvis/domainstack.io"
href={`https://github.com/${REPOSITORY_SLUG}`}
target="_blank"
rel="noopener"
className="group flex select-none items-center gap-2 transition-colors"

View File

@@ -1,7 +1,6 @@
import "server-only";
import * as ipaddr from "ipaddr.js";
import { cacheLife, cacheTag } from "next/cache";
import { CLOUDFLARE_IPS_URL } from "@/lib/constants/external-apis";
import { ipV4InCidr, ipV6InCidr } from "@/lib/ip";
import { createLogger } from "@/lib/logger/server";
@@ -20,7 +19,12 @@ let lastLoadedIpv6Parsed: Array<[ipaddr.IPv6, number]> | undefined;
* Fetch Cloudflare IP ranges from their API.
*/
async function fetchCloudflareIpRanges(): Promise<CloudflareIpRanges> {
const res = await fetch(CLOUDFLARE_IPS_URL);
const res = await fetch(CLOUDFLARE_IPS_URL, {
next: {
revalidate: 604800, // 1 week
tags: ["cloudflare-ip-ranges"],
},
});
if (!res.ok) {
throw new Error(`Failed to fetch Cloudflare IPs: ${res.status}`);
@@ -28,6 +32,8 @@ async function fetchCloudflareIpRanges(): Promise<CloudflareIpRanges> {
const data = await res.json();
logger.info("IP ranges fetched");
return {
ipv4Cidrs: data.result?.ipv4_cidrs || [],
ipv6Cidrs: data.result?.ipv6_cidrs || [],
@@ -79,18 +85,11 @@ function parseAndCacheRanges(ranges: CloudflareIpRanges): void {
*
* The IP ranges change infrequently (when Cloudflare expands infrastructure),
* so we cache for 1 week with stale-while-revalidate.
*
* Uses Next.js 16's "use cache" directive for automatic caching with tags.
*/
async function getCloudflareIpRanges(): Promise<CloudflareIpRanges> {
"use cache";
cacheLife("weeks");
cacheTag("cloudflare-ip-ranges");
try {
const ranges = await fetchCloudflareIpRanges();
parseAndCacheRanges(ranges);
logger.info("IP ranges fetched");
return ranges;
} catch (err) {
logger.error("fetch error", err);

View File

@@ -1,6 +1,5 @@
import "server-only";
import { cacheLife, cacheTag } from "next/cache";
import type { BootstrapData } from "rdapper";
import { RDAP_BOOTSTRAP_URL } from "@/lib/constants/external-apis";
import { createLogger } from "@/lib/logger/server";
@@ -20,11 +19,12 @@ const logger = createLogger({ source: "rdap-bootstrap" });
* @throws Error if fetch fails (caller should handle or let rdapper fetch directly)
*/
export async function getRdapBootstrapData(): Promise<BootstrapData> {
"use cache";
cacheLife("weeks");
cacheTag("rdap-bootstrap");
const res = await fetch(RDAP_BOOTSTRAP_URL);
const res = await fetch(RDAP_BOOTSTRAP_URL, {
next: {
revalidate: 604800, // 1 week
tags: ["rdap-bootstrap"],
},
});
if (!res.ok) {
throw new Error(

View File

@@ -9,6 +9,6 @@ export const config = {
matcher: [
// Exclude API and Next internals/static assets for performance and to avoid side effects
// Static files use (?:[?#]|$) to match exactly (not as prefixes) so domains like "favicon.icon.com" are not excluded
"/((?!api/|_next/|_vercel/|_proxy/|(?:favicon.ico|icon.svg|robots.txt|sitemap.xml|manifest.webmanifest)(?:[?#]|$)).*)",
"/((?!api/|_next/|_vercel/|_proxy/|(?:favicon.ico|icon.svg|robots.txt|sitemap.xml|manifest.webmanifest|opensearch.xml)(?:[?#]|$)).*)",
],
};

View File

@@ -1,4 +1,3 @@
import { cacheLife, cacheTag } from "next/cache";
import { getDomainTld } from "rdapper";
import { createLogger } from "@/lib/logger/server";
import type { Pricing } from "@/lib/schemas";
@@ -9,7 +8,7 @@ const logger = createLogger({ source: "pricing" });
* Domain registration pricing service.
*
* Caching Strategy:
* - Uses Next.js 16 Data Cache with "use cache" directive
* - Uses Next.js Data Cache with `fetch` configuration
* - Automatic stale-while-revalidate (SWR): serves cached data instantly,
* revalidates in background when cache expires
* - Cache TTLs: 7 days (Porkbun and Cloudflare)
@@ -46,7 +45,6 @@ interface PricingProvider {
/**
* Fetch pricing data from a provider with Next.js Data Cache.
* Uses Next.js 16's "use cache" directive for automatic caching.
*/
async function fetchProviderPricing(
provider: PricingProvider,
@@ -71,10 +69,6 @@ const porkbunProvider: PricingProvider = {
name: "porkbun",
async fetchPricing(): Promise<RegistrarPricingResponse> {
"use cache";
cacheLife("weeks");
cacheTag("pricing", "pricing-porkbun");
// Does not require authentication!
// https://porkbun.com/api/json/v3/documentation#Domain%20Pricing
const controller = new AbortController();
@@ -88,6 +82,10 @@ const porkbunProvider: PricingProvider = {
headers: { "Content-Type": "application/json" },
body: "{}",
signal: controller.signal,
next: {
revalidate: 604800,
tags: ["pricing", "pricing-porkbun"],
},
},
);
@@ -124,10 +122,6 @@ const cloudflareProvider: PricingProvider = {
name: "cloudflare",
async fetchPricing(): Promise<RegistrarPricingResponse> {
"use cache";
cacheLife("weeks");
cacheTag("pricing", "pricing-cloudflare");
// Third-party API that aggregates Cloudflare pricing
// https://cfdomainpricing.com/
const controller = new AbortController();
@@ -138,6 +132,10 @@ const cloudflareProvider: PricingProvider = {
method: "GET",
headers: { Accept: "application/json" },
signal: controller.signal,
next: {
revalidate: 604800,
tags: ["pricing", "pricing-cloudflare"],
},
});
if (!res.ok) {