Setting the file. One moment. Fetch · Eas Workflows · expo/skills · Skills Docsscripts/fetch.js
JavaScript·109 lines·3 KB
);
9const DEFAULT_TTL_SECONDS = 15 * 60; // 15 minutes
10
11export async function fetchCached(url) {
12 await mkdir(CACHE_DIRECTORY, { recursive: true });
13
14 const cacheFile = resolve(CACHE_DIRECTORY, hashUrl(url) + '.json');
15 const cached = await loadCacheEntry(cacheFile);
16 if (cached && cached.expires > Math.floor(Date.now() / 1000)) {
17 return cached.data;
18 }
19
20 // Make request, with conditional If-None-Match if we have an ETag.
21 // Cache-Control: max-age=0 overrides Node's default 'no-cache' to allow 304 responses.
22 const response = await fetch(url, {
23 headers: {
24 'Cache-Control': 'max-age=0',
25 ...(cached?.etag && { 'If-None-Match': cached.etag }),
26 },
27 });
28
29 if (response.status === 304 && cached) {
30 // Refresh expiration and return cached data
31 const entry = { ...cached, expires: getExpires(response.headers) };
32 await saveCacheEntry(cacheFile, entry);
33 return cached.data;
34 }
35
36 if (!response.ok) {
37 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
38 }
39
40 const etag = response.headers.get('etag');
41 const data = await response.text();
42 const expires = getExpires(response.headers);
43
44 await saveCacheEntry(cacheFile, { url, etag, expires, data });
45
46 return data;
47}
48
49function hashUrl(url) {
50 return createHash('sha256').update(url).digest('hex').slice(0, 16);
51}
52
53async function loadCacheEntry(cacheFile) {
54 try {
55 return JSON.parse(await readFile(cacheFile, 'utf-8'));
56 } catch {
57 return null;
58 }
59}
60
61async function saveCacheEntry(cacheFile, entry) {
62 await writeFile(cacheFile, JSON.stringify(entry, null, 2));
63}
64
65function getExpires(headers) {
66 const now = Math.floor(Date.now() / 1000);
67
68 // Prefer Cache-Control: max-age
69 const maxAgeSeconds = parseMaxAge(headers.get('cache-control'));
70 if (maxAgeSeconds != null) {
71 return now + maxAgeSeconds;
72 }
73
74 // Fall back to Expires header
75 const expires = headers.get('expires');
76 if (expires) {
77 const expiresTime = Date.parse(expires);
78 if (!Number.isNaN(expiresTime)) {
79 return Math.floor(expiresTime / 1000);
80 }
81 }
82
83 // Default TTL
84 return now + DEFAULT_TTL_SECONDS;
85}
86
87function parseMaxAge(cacheControl) {
88 if (!cacheControl) {
89 return null;
90 }
91 const match = cacheControl.match(/max-age=(\d+)/i);
92 return match ? parseInt(match[1], 10) : null;
93}
94
95if (import.meta.main) {
96 const url = process.argv[2];
97
98 if (!url || url === '--help' || url === '-h') {
99 console.log(`Usage: fetch <url>
100
101Fetches a URL with HTTP caching (ETags + Cache-Control/Expires).
102Default TTL: ${DEFAULT_TTL_SECONDS / 60} minutes.
103Cache is stored in: ${CACHE_DIRECTORY}/`);
104 process.exit(url ? 0 : 1);
105 }
106
107 const data = await fetchCached(url);
108 console.log(data);
109}