forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1/**
2 * Configuration for the stale-while-revalidate fetch cache.
3 *
4 * This cache intercepts external API calls during SSR and caches responses
5 * using Nitro's storage layer (backed by Vercel's runtime cache in production).
6 */
7
8import { CONSTELLATION_HOST, SLINGSHOT_HOST } from './constants'
9
10/**
11 * Domains that should have their fetch responses cached.
12 * Only requests to these domains will be intercepted and cached.
13 */
14export const FETCH_CACHE_ALLOWED_DOMAINS = [
15 // npm registry
16 'registry.npmjs.org', // npm package metadata (packuments)
17 'api.npmjs.org', // npm download statistics
18
19 // JSR registry
20 'jsr.io', // JSR package metadata
21
22 // Git hosting providers (for repo metadata)
23 'ungh.cc', // GitHub proxy (avoids rate limits)
24 'api.github.com', // GitHub API
25 'gitlab.com', // GitLab API
26 'api.bitbucket.org', // Bitbucket API
27 'codeberg.org', // Codeberg (Gitea-based)
28 'gitee.com', // Gitee API
29 // microcosm endpoints for atproto data
30 CONSTELLATION_HOST,
31 SLINGSHOT_HOST,
32] as const
33
34/**
35 * Default TTL for cached fetch responses (in seconds).
36 * After this time, cached data is considered "stale" but will still be
37 * returned immediately while a background revalidation occurs.
38 */
39export const FETCH_CACHE_DEFAULT_TTL = 60 * 5 // 5 minutes
40
41/**
42 * Cache key version prefix.
43 * Increment this to invalidate all cached entries (e.g., after format changes).
44 */
45export const FETCH_CACHE_VERSION = 'v1'
46
47/**
48 * Storage key prefix for fetch cache entries.
49 */
50export const FETCH_CACHE_STORAGE_BASE = 'fetch-cache'
51
52/**
53 * Check if a URL's host is in the allowed domains list.
54 */
55export function isAllowedDomain(url: string | URL): boolean {
56 try {
57 const urlObj = typeof url === 'string' ? new URL(url) : url
58 return FETCH_CACHE_ALLOWED_DOMAINS.some(domain => urlObj.host === domain)
59 } catch {
60 return false
61 }
62}
63
64/**
65 * Structure of a cached fetch entry stored in Nitro storage.
66 */
67export interface CachedFetchEntry<T = unknown> {
68 /** The response body/data */
69 data: T
70 /** HTTP status code */
71 status: number
72 /** Response headers (subset) */
73 headers: Record<string, string>
74 /** Unix timestamp when the entry was cached */
75 cachedAt: number
76 /** TTL in seconds */
77 ttl: number
78}
79
80/**
81 * Check if a cached entry is stale (past its TTL).
82 */
83export function isCacheEntryStale(entry: CachedFetchEntry): boolean {
84 const now = Date.now()
85 const expiresAt = entry.cachedAt + entry.ttl * 1000
86 return now > expiresAt
87}
88
89/**
90 * Result returned by cachedFetch with staleness metadata.
91 * This allows consumers to know if the data came from stale cache
92 * and potentially trigger client-side revalidation.
93 */
94export interface CachedFetchResult<T> {
95 /** The response data */
96 data: T
97 /** Whether the data came from stale cache (past TTL) */
98 isStale: boolean
99 /** Unix timestamp when the data was cached, or null if fresh fetch */
100 cachedAt: number | null
101}
102
103/**
104 * Type for the cachedFetch function attached to event context.
105 */
106export type CachedFetchFunction = <T = unknown>(
107 url: string,
108 options?: Parameters<typeof $fetch>[1],
109 ttl?: number,
110) => Promise<CachedFetchResult<T>>