Skip to content

Commit 1837032

Browse files
Simplify Cloudflare worker.
1 parent e5ae970 commit 1837032

1 file changed

Lines changed: 39 additions & 94 deletions

File tree

cloudflare-workers/gumroad-products/worker.js

Lines changed: 39 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
1-
// worker.js
1+
// worker.js (simplified, no explicit caching)
2+
23
const DEFAULT_ALLOWED_ORIGINS = [
34
"https://tools.mathspp.com",
45
"http://localhost:5173",
56
"http://localhost:3000",
67
];
78

8-
const S_MAX_AGE = 3600; // 1h fresh cache
9-
const STALE_WINDOW = 24 * 3600; // 24h serve-stale if upstream errors
10-
119
function buildAllowedOrigins(env) {
1210
const allowList = (env?.ALLOWED_ORIGINS || DEFAULT_ALLOWED_ORIGINS.join(","))
13-
.split(",").map(s => s.trim()).filter(Boolean);
11+
.split(",")
12+
.map((s) => s.trim())
13+
.filter(Boolean);
1414
return new Set(allowList);
1515
}
16+
1617
function corsHeaders(origin, allowed) {
1718
const allow = allowed.has(origin) ? origin : "";
1819
return {
1920
"Access-Control-Allow-Origin": allow,
2021
"Access-Control-Allow-Methods": "GET, OPTIONS",
2122
"Access-Control-Allow-Headers": "Content-Type",
22-
"Vary": "Origin",
23+
Vary: "Origin",
2324
};
2425
}
26+
2527
function respondJSON(origin, allowed, data, status = 200, extra = {}) {
2628
return new Response(JSON.stringify(data), {
2729
status,
@@ -32,8 +34,12 @@ function respondJSON(origin, allowed, data, status = 200, extra = {}) {
3234
},
3335
});
3436
}
35-
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
3637

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

6066
export default {
61-
async fetch(request, env, ctx) {
67+
async fetch(request, env) {
6268
const origin = request.headers.get("Origin") || "";
6369
const allowed = buildAllowedOrigins(env);
6470
const url = new URL(request.url);
@@ -70,7 +76,9 @@ export default {
7076
return respondJSON(origin, allowed, { error: "Method Not Allowed" }, 405);
7177
}
7278

73-
const goodPath = url.pathname === "/api/gumroad-products" || url.pathname === "/api/gumroad-products/";
79+
const goodPath =
80+
url.pathname === "/api/gumroad-products" ||
81+
url.pathname === "/api/gumroad-products/";
7482
if (!goodPath) {
7583
return respondJSON(origin, allowed, { error: "Not Found" }, 404);
7684
}
@@ -80,119 +88,54 @@ export default {
8088
return respondJSON(origin, allowed, { error: "Invalid or missing Gumroad username." }, 400);
8189
}
8290

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
91+
// Try subdomain profile, then path-based profile as fallback
10992
const profileUrlA = `https://${u}.gumroad.com/`;
11093
const profileUrlB = `https://gumroad.com/${encodeURIComponent(u)}`;
11194

11295
async function getProfileHTML(urlStr) {
113-
const res = await fetchWithBackoff(urlStr, {
96+
return fetchWithBackoff(urlStr, {
11497
redirect: "follow",
11598
headers: {
116-
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
99+
Accept:
100+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
117101
"Accept-Language": "en-US,en;q=0.9",
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",
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/",
122105
},
123106
});
124-
return res;
125107
}
126108

127-
// 1) Try subdomain and then path profile if it fails.
128109
let upstream = await getProfileHTML(profileUrlA);
129110
if (!upstream.ok && (upstream.status === 429 || upstream.status === 403)) {
130111
upstream = await getProfileHTML(profileUrlB);
131112
}
132-
133-
// If still not ok, serve stale or bubble error
134113
if (!upstream.ok) {
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-
});
114+
return respondJSON(
115+
origin,
116+
allowed,
117+
{ error: "Upstream error", status: upstream.status },
118+
upstream.status
119+
);
152120
}
153121

154122
const html = await upstream.text();
155-
156123
const products = extractProducts(html);
157124

158125
const payload = {
159126
username: u,
160-
profile_url: profileUrl,
127+
profile_url: `https://${u}.gumroad.com/`,
161128
count: products.length,
162129
products,
163130
fetched_at: new Date().toISOString(),
164131
};
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-
});
132+
133+
return respondJSON(origin, allowed, payload, 200);
190134
},
191135
};
192136

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

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

212-
const key = url + '|' + title;
157+
const key = url + "|" + title;
213158
if (seen.has(key)) continue;
214159
seen.add(key);
215160

0 commit comments

Comments
 (0)