You've already forked domainstack.io
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:
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
2
proxy.ts
2
proxy.ts
@@ -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)(?:[?#]|$)).*)",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user