1- // worker.js
1+ // worker.js (simplified, no explicit caching)
2+
23const 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-
119function 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+
1617function 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+
2527function 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.
3743async 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
6066export 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
193137function 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 [ ^ > ] * h r e f = [ " ' ] ( [ ^ " ' ] * \/ l \/ [ ^ " ' ] * ) [ " ' ] [ ^ > ] * > ( [ \s \S ] * ?) < \/ a > / gi;
197140 const tagRe = / < \/ ? [ ^ > ] + > / g;
198141 const nbspRe = / & n b s p ; / 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