Skip to content

Commit 4319066

Browse files
Revert worker simplification.
1 parent a153035 commit 4319066

1 file changed

Lines changed: 94 additions & 39 deletions

File tree

cloudflare-workers/gumroad-products/worker.js

Lines changed: 94 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,27 @@
1-
// worker.js (simplified, no explicit caching)
2-
1+
// worker.js
32
const DEFAULT_ALLOWED_ORIGINS = [
43
"https://tools.mathspp.com",
54
"http://localhost:5173",
65
"http://localhost:3000",
76
];
87

8+
const S_MAX_AGE = 3600; // 1h fresh cache
9+
const STALE_WINDOW = 24 * 3600; // 24h serve-stale if upstream errors
10+
911
function buildAllowedOrigins(env) {
1012
const allowList = (env?.ALLOWED_ORIGINS || DEFAULT_ALLOWED_ORIGINS.join(","))
11-
.split(",")
12-
.map((s) => s.trim())
13-
.filter(Boolean);
13+
.split(",").map(s => s.trim()).filter(Boolean);
1414
return new Set(allowList);
1515
}
16-
1716
function corsHeaders(origin, allowed) {
1817
const allow = allowed.has(origin) ? origin : "";
1918
return {
2019
"Access-Control-Allow-Origin": allow,
2120
"Access-Control-Allow-Methods": "GET, OPTIONS",
2221
"Access-Control-Allow-Headers": "Content-Type",
23-
Vary: "Origin",
22+
"Vary": "Origin",
2423
};
2524
}
26-
2725
function respondJSON(origin, allowed, data, status = 200, extra = {}) {
2826
return new Response(JSON.stringify(data), {
2927
status,
@@ -34,12 +32,8 @@ function respondJSON(origin, allowed, data, status = 200, extra = {}) {
3432
},
3533
});
3634
}
35+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
3736

38-
function sleep(ms) {
39-
return new Promise((r) => setTimeout(r, ms));
40-
}
41-
42-
// Simple backoff for 429/503; not related to caching.
4337
async function fetchWithBackoff(url, init, attempts = 3) {
4438
for (let i = 0; i < attempts; i++) {
4539
const res = await fetch(url, init);
@@ -64,7 +58,7 @@ async function fetchWithBackoff(url, init, attempts = 3) {
6458
}
6559

6660
export default {
67-
async fetch(request, env) {
61+
async fetch(request, env, ctx) {
6862
const origin = request.headers.get("Origin") || "";
6963
const allowed = buildAllowedOrigins(env);
7064
const url = new URL(request.url);
@@ -76,9 +70,7 @@ export default {
7670
return respondJSON(origin, allowed, { error: "Method Not Allowed" }, 405);
7771
}
7872

79-
const goodPath =
80-
url.pathname === "/api/gumroad-products" ||
81-
url.pathname === "/api/gumroad-products/";
73+
const goodPath = url.pathname === "/api/gumroad-products" || url.pathname === "/api/gumroad-products/";
8274
if (!goodPath) {
8375
return respondJSON(origin, allowed, { error: "Not Found" }, 404);
8476
}
@@ -88,54 +80,119 @@ export default {
8880
return respondJSON(origin, allowed, { error: "Invalid or missing Gumroad username." }, 400);
8981
}
9082

91-
// Try subdomain profile, then path-based profile as fallback
83+
const profileUrl = `https://${u}.gumroad.com/`;
84+
const cache = caches.default;
85+
const dataKey = new Request(`https://gumroad-products.internal/cache?u=${encodeURIComponent(u)}`);
86+
const metaKey = new Request(`https://gumroad-products.internal/meta?u=${encodeURIComponent(u)}`);
87+
88+
let cachedBody = null, cachedTs = 0;
89+
const c = await cache.match(dataKey);
90+
if (c) cachedBody = await c.text();
91+
const cm = await cache.match(metaKey);
92+
if (cm) try { cachedTs = (await cm.json()).ts || 0; } catch { }
93+
94+
// Serve fresh cache (<= 1h)
95+
const age = Math.floor(Date.now() / 1000) - cachedTs;
96+
if (cachedBody && age <= S_MAX_AGE) {
97+
return new Response(cachedBody, {
98+
headers: {
99+
"Content-Type": "application/json; charset=utf-8",
100+
"Cache-Control": `public, max-age=${S_MAX_AGE}`,
101+
"X-Cache": "HIT",
102+
"X-Upstream-Status": "none",
103+
...corsHeaders(origin, allowed),
104+
},
105+
});
106+
}
107+
108+
// Try subdomain first, then path-based profile
92109
const profileUrlA = `https://${u}.gumroad.com/`;
93110
const profileUrlB = `https://gumroad.com/${encodeURIComponent(u)}`;
94111

95112
async function getProfileHTML(urlStr) {
96-
return fetchWithBackoff(urlStr, {
113+
const res = await fetchWithBackoff(urlStr, {
97114
redirect: "follow",
98115
headers: {
99-
Accept:
100-
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
116+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
101117
"Accept-Language": "en-US,en;q=0.9",
102-
"User-Agent":
103-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
104-
Referer: "https://gumroad.com/",
118+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
119+
"Referer": "https://gumroad.com/",
120+
"Cache-Control": "no-cache",
121+
"Pragma": "no-cache",
105122
},
106123
});
124+
return res;
107125
}
108126

127+
// 1) Try subdomain and then path profile if it fails.
109128
let upstream = await getProfileHTML(profileUrlA);
110129
if (!upstream.ok && (upstream.status === 429 || upstream.status === 403)) {
111130
upstream = await getProfileHTML(profileUrlB);
112131
}
132+
133+
// If still not ok, serve stale or bubble error
113134
if (!upstream.ok) {
114-
return respondJSON(
115-
origin,
116-
allowed,
117-
{ error: "Upstream error", status: upstream.status },
118-
upstream.status
119-
);
135+
// Serve stale (<= 25h old total) instead of failing
136+
if (cachedBody && age <= (S_MAX_AGE + STALE_WINDOW)) {
137+
return new Response(cachedBody, {
138+
headers: {
139+
"Content-Type": "application/json; charset=utf-8",
140+
"Cache-Control": `public, max-age=0, stale-while-revalidate=${STALE_WINDOW}`,
141+
"X-Cache": "STALE",
142+
"X-Upstream-Status": String(upstream.status),
143+
...corsHeaders(origin, allowed),
144+
},
145+
});
146+
}
147+
// No cache to fall back to
148+
return respondJSON(origin, allowed, { error: "Upstream error", status: upstream.status }, upstream.status, {
149+
"X-Cache": "MISS",
150+
"X-Upstream-Status": String(upstream.status),
151+
});
120152
}
121153

122154
const html = await upstream.text();
155+
123156
const products = extractProducts(html);
124157

125158
const payload = {
126159
username: u,
127-
profile_url: `https://${u}.gumroad.com/`,
160+
profile_url: profileUrl,
128161
count: products.length,
129162
products,
130163
fetched_at: new Date().toISOString(),
131164
};
132-
133-
return respondJSON(origin, allowed, payload, 200);
165+
const body = JSON.stringify(payload);
166+
167+
// Update cache
168+
const now = Math.floor(Date.now() / 1000);
169+
const dataResp = new Response(body, {
170+
headers: {
171+
"Content-Type": "application/json; charset=utf-8",
172+
"Cache-Control": `public, max-age=${S_MAX_AGE}`,
173+
},
174+
});
175+
const metaResp = new Response(JSON.stringify({ ts: now }), {
176+
headers: { "Content-Type": "application/json" },
177+
});
178+
ctx.waitUntil(cache.put(dataKey, dataResp.clone()));
179+
ctx.waitUntil(cache.put(metaKey, metaResp.clone()));
180+
181+
return new Response(body, {
182+
headers: {
183+
"Content-Type": "application/json; charset=utf-8",
184+
"Cache-Control": `public, max-age=${S_MAX_AGE}`,
185+
"X-Cache": cachedBody ? "MISS-REVAL" : "MISS",
186+
"X-Upstream-Status": "200",
187+
...corsHeaders(origin, allowed),
188+
},
189+
});
134190
},
135191
};
136192

137193
function extractProducts(html) {
138194
// Lightweight HTML parsing without DOM: heuristic regex over links.
195+
// For more robustness, you could use an HTML parser lib with Workers Bundler.
139196
const linkRe = /<a\b[^>]*href=["']([^"']*\/l\/[^"']*)["'][^>]*>([\s\S]*?)<\/a>/gi;
140197
const tagRe = /<\/?[^>]+>/g;
141198
const nbspRe = /&nbsp;/g;
@@ -145,16 +202,14 @@ function extractProducts(html) {
145202
let m;
146203
while ((m = linkRe.exec(html)) !== null) {
147204
const href = m[1];
148-
let title = m[2].replace(tagRe, "").replace(nbspRe, " ").trim();
205+
let title = m[2].replace(tagRe, '').replace(nbspRe, ' ').trim();
149206
if (!title) continue;
150207

151208
// Normalize absolute vs relative
152-
const url = href.startsWith("http")
153-
? href
154-
: new URL(href, "https://example.com").href;
155-
const slug = url.split("/").filter(Boolean).pop();
209+
const url = href.startsWith('http') ? href : new URL(href, 'https://example.com').href;
210+
const slug = url.split('/').filter(Boolean).pop();
156211

157-
const key = url + "|" + title;
212+
const key = url + '|' + title;
158213
if (seen.has(key)) continue;
159214
seen.add(key);
160215

0 commit comments

Comments
 (0)